from utils_json import DEFAULT_MAX_PATH_LIMIT, SIZE_64MB, \
    DEFAULT_WIKIPEDIA_COMPACT_PATH, DEFAULT_WIKIPEDIA_PATH, \
    JSON_INFO_METRICS_SECTION, JSON_INFO_NAMES
from valkey.exceptions import ResponseError, NoPermissionError
from valkeytests.conftest import resource_port_tracker
import pytest
import glob
import logging
import os
import random
import struct
import json
from math import isclose, isnan, isinf, frexp
from json_test_case import JsonTestCase
from error_handlers import ErrorStringTester

logging.basicConfig(level=logging.DEBUG)

DATA_ORGANISM = '''
            {
                "heavy_animal" : 200,
                "plants" : [
                  {
                    "name"   : "Cactus",
                    "height" : 120,
                    "weight" : 90
                  },
                  {
                    "name"   : "Ghost Plant",
                    "height" : "Unknown",
                    "weight" : "Unknown"
                  },
                  {
                    "name"   : "Redwood",
                    "height" : 4200,
                    "weight" : 50000
                  }
                ],
                "animals" : [
                  {
                    "name"   : "Platypus",
                    "length" : 24,
                    "weight" : 5
                  },
                  {
                    "fish" : [
                      {
                        "name"   : "Bass",
                        "length" : 34,
                        "weight" : 5
                      },
                      {
                        "name"   : "Swordfish",
                        "length" : 177,
                        "weight" : 200
                      }
            
                    ]
                  },
                  {
                    "mammals" : [
                      {
                        "name"   : "Platypus",
                        "length" : 24,
                        "weight" : 5
                      },
                      {
                        "name"   : "Horse",
                        "height" : 68,
                        "weight" : 660
                      },
                      {
                        "primates" : [
                          {
                            "name"   : "Monkey",
                            "height" : 18,
                            "weight" : 30
                          },
                          {
                            "name"   : "Baboon",
                            "height" : 26,
                            "weight" : 50
                          },
                          {
                            "apes" : [
                              {
                                "name"   : "Chimpanzee",
                                "height" : 66,
                                "weight" : 130
                              },
                              {
                                "name"   : "Gorilla",
                                "height" : 66,
                                "weight" : 400
                              },
                              {
                                "name"   : "Orangutan",
                                "height" : 70,
                                "weight" : 300
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            '''

# valkey keys
wikipedia = 'wikipedia'
wikipedia2 = 'wikipedia2'
wikipedia3 = 'wikipedia3'
wikipedia4 = 'wikipedia4'
store = 'store'
store2 = 'store2'
organism = 'organism'
organism2 = 'organism2'
str_key = 'str_key'
k1 = 'k1'
k2 = 'k2'
k3 = 'k3'
k4 = 'k4'
k5 = 'k5'
k6 = 'k6'
k7 = 'k7'
k8 = 'k8'
k9 = 'k9'
k10 = 'k10'
k11 = 'k11'
k12 = 'k12'
k = 'k'
nonexistentkey = 'nonexistentkey'
nonexistentpath = 'nonexistentpath'
input = 'input'
input2 = 'input2'
arr = 'arr'
foo = 'foo'
baz = 'baz'


# Base Test class containing all core json module tests
class TestJsonBasic(JsonTestCase):

    def setup_data(self):
        client = self.server.get_new_client()
        client.config_set('json.max-path-limit', DEFAULT_MAX_PATH_LIMIT)
        client.config_set('json.max-document-size', SIZE_64MB)
        # Need the following line when executing the test against a running Valkey.
        # Otherwise, data from previous test cases will interfere current test case.
        client.execute_command("FLUSHDB")

        # Load wikipedia sample JSONs. We use wikipedia.json as input to create a document key. Then, use
        # wikipedia_compact.json, which does not have indent/space/newline, to verify correctness of serialization.
        with open(DEFAULT_WIKIPEDIA_PATH, 'r') as file:
            self.data_wikipedia = file.read()
        with open(DEFAULT_WIKIPEDIA_COMPACT_PATH, 'r') as file:
            self.data_wikipedia_compact = file.read()
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.', self.data_wikipedia)

        # Create a string key to be used for verifying that JSON.GET should not operate on a non-document key.
        client.execute_command(
            'SET', str_key, '{"firstName":"John","lastName":"Smith"}')

    @pytest.fixture(autouse=True)
    def setup_test(self, setup):
        # Check if we should use external server
        use_external = os.environ.get("VALKEY_EXTERNAL_SERVER", "false").lower() == "true"
        
        if use_external:
            # Use external server
            external_host = os.environ.get("VALKEY_HOST", "localhost")
            external_port = int(os.environ.get("VALKEY_PORT", "6379"))
            self.server, self.client = self.create_server(
                testdir=self.testdir,
                bind_ip=external_host,
                port=external_port,
                external_server=True
            )
        else:
            # Original local server setup
            server_path = f"{os.path.dirname(os.path.realpath(__file__))}/.build/binaries/{os.environ['SERVER_VERSION']}/valkey-server"
            args = {'loadmodule': os.getenv('MODULE_PATH'), "enable-debug-command": "local", 'enable-protected-configs': 'yes'}
            self.server, self.client = self.create_server(testdir=self.testdir, server_path=server_path, args=args)

        self.error_class = ErrorStringTester
        self.setup_data()

    def test_sanity(self):
        '''
        Test simple SET/GET/MGET/DEL, both legacy and JSONPath syntax.
        '''

        client = self.server.get_new_client()
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', '{"a":"1", "b":"2", "c":"3"}')

        assert b'OK' == client.execute_command(
            'JSON.SET', k2, '.', '[1,2,3,4,5]')
        assert b'OK' == client.execute_command(
            'JSON.MSET', k3, '.', '[1,2,3,4,5]')
        assert b'OK' == client.execute_command(
            'JSON.MSET', k2, '.', '[5,4,3,2,1]', k1, '.', '{"a":"1", "b":"2", "c":"3"}')
        assert [b'{"a":"1","b":"2","c":"3"}', b'[5,4,3,2,1]', b'[1,2,3,4,5]'] == client.execute_command(
            'JSON.MGET', k1, k2, k3, '.')
        assert b'OK' == client.execute_command(
            'JSON.SET', k2, '.', '[1,2,3,4,5]')
        assert b'{"a":"1","b":"2","c":"3"}' == client.execute_command(
            'JSON.GET', k1)
        for (key, path, exp) in [
            (k1, '.',    '{"a":"1","b":"2","c":"3"}'),
            (k1, '.a',   '"1"'),
            (k2, '.',    '[1,2,3,4,5]'),
            (k2, '[-1]', '5')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path).decode()
        # pretty print
        for (key, fmt, path, exp) in [
            (k1, 'SPACE',  '.',       '{"a": "1","b": "2","c": "3"}'),
            (k1, 'INDENT', '.',       '{ "a":"1", "b":"2", "c":"3"}'),
            (k2, 'INDENT', '.',       '[ 1, 2, 3, 4, 5]'),
            (k2, 'INDENT', '[-2]',    '4')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, fmt, ' ', path).decode()

        assert [b'[{"a":"1","b":"2","c":"3"}]', b'[[1,2,3,4,5]]'] == client.execute_command(
            'JSON.MGET', k1, k2, '$')
        for (key, path, exp) in [
            (k1, '$',       '[{"a":"1","b":"2","c":"3"}]'),
            (k1, '$.*',     '["1","2","3"]'),
            (k2, '$',       '[[1,2,3,4,5]]'),
            (k2, '$.[*]',   '[1,2,3,4,5]'),
            (k2, '$.[-1]',  '[5]'),
            (k2, '$.[0:3]', '[1,2,3]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path).decode()
        # pretty print
        for (key, fmt, path, exp) in [
            (k1, 'SPACE',  '$',       '[{"a": "1","b": "2","c": "3"}]'),
            (k1, 'INDENT', '$',
                '[ {  "a":"1",  "b":"2",  "c":"3" }]'),
            (k1, 'INDENT', '$.*',     '[ "1", "2", "3"]'),
            (k2, 'INDENT', '$',       '[ [  1,  2,  3,  4,  5 ]]'),
            (k2, 'INDENT', '$.[0:3]', '[ 1, 2, 3]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, fmt, ' ', path).decode()

        assert 1 == client.execute_command('JSON.DEL', k1)
        assert 1 == client.execute_command('JSON.DEL', k2)
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '$', '{"a":"1", "b":"2", "c":"3"}')
        assert b'OK' == client.execute_command(
            'JSON.SET', k2, '$', '[1,2,3,4,5]')
        for (key, val_before, del_path, del_ret, val_after) in [
            (k1, '[{"a":"1","b":"2","c":"3"}]', '$.*',   3, '[{}]'),
            (k2, '[[1,2,3,4,5]]',               '$.[*]', 5, '[[]]')
        ]:
            assert val_before == client.execute_command(
                'JSON.GET', key, '$').decode()
            assert del_ret == client.execute_command(
                'JSON.DEL', key, del_path)
            assert val_after == client.execute_command(
                'JSON.GET', key, '$').decode()

    def test_parse_invalid_json_string(self):
        client = self.server.get_new_client()
        for input_str in ['foo', '{"a"}', '[a]']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.SET', wikipedia, '.firstName', input_str)
            assert self.error_class.is_syntax_error(str(e.value))

    def test_json_set_command_supports_all_datatypes(self):
        client = self.server.get_new_client()
        for (path, value) in [('.address.city', '"Boston"'),            # string
                              # number
                              ('.age', '30'),
                              ('.foo', '[1,2,3]'),                      # array
                              # array element access
                              ('.phoneNumbers[0].number', '"1234567"'),
                              # object
                              ('.foo', '{"a":"b"}'),
                              ('.lastName', 'null'),                    # null
                              ('.isAlive', 'false')]:                   # boolean
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, path, value)
            assert value.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

    def test_json_set_command_root_document(self):
        client = self.server.get_new_client()
        # path to the root document is represented as '.'
        for (key, value) in [(k1, '"Boston"'),                # string
                             (k2, '123'),                     # number
                             (k3, '["Seattle","Boston"]'),    # array
                             (k3, '[1,2,3]'),                 # array
                             (k4, '{"a":"b"}'),               # object
                             (k4, '{}'),                      # empty object
                             (k5, 'null'),                    # null
                             (k6, 'false')]:                  # boolean
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '.', value)
            assert value.encode() == client.execute_command('JSON.GET', key)

    def test_json_set_command_nx_xx_options(self):
        client = self.server.get_new_client()
        for (path, value, cond, exp_set_return, exp_get_return) in [
            ('.address.city', '"Boston"', 'NX', None, b'"New York"'),
            ('.address.city', '"Boston"', 'XX', b'OK', b'"Boston"'),
            ('.foo', '"bar"', 'NX', b'OK', b'"bar"'),
                ('.firstName', '"bar"', 'NX', None, b'"John"')]:
            assert exp_set_return == client.execute_command(
                'JSON.SET', wikipedia, path, value, cond)
            assert exp_get_return == client.execute_command(
                'JSON.GET', wikipedia, path)

            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.SET', k1, '.' '"value"', 'badword')
            assert self.error_class.is_syntax_error(str(e.value))

    def test_json_set_command_with_error_conditions(self):
        client = self.server.get_new_client()
        # A new Valkey key's path must be root
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.SET', foo, '.bar', '"bar"')
        assert self.error_class.is_syntax_error(str(e.value))

        # Option XX means setting the value only if the JSON path exists, a.k.a, updating the value.
        # According to API, if the value does not exist, the command should return null instead of error.
        assert None == client.execute_command(
            'JSON.SET', k1, '.', '"some value"', 'XX')
        assert None == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '"bar"', 'XX')
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.GET', wikipedia, '.foo')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # Option NX means setting the value only if the JSON path does not exists, a.k.a, inserting the value.
        # According to API, if the value exists, the command should return null instead of error.
        assert None == client.execute_command(
            'JSON.SET', wikipedia, '.', '"some new value"', 'NX')
        assert None == client.execute_command(
            'JSON.SET', wikipedia, '.firstName', '"Tom"', 'NX')
        assert b'"John"' == client.execute_command(
            'JSON.GET', wikipedia, '.firstName')

        # syntax error: wrong option
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, '.', '"some new value"', 'NXXX')
        assert self.error_class.is_syntax_error(str(e.value))
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, '.', '"some new value"', 'XXXX')
        assert self.error_class.is_syntax_error(str(e.value))
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.SET', wikipedia, '.', '"bar"', 'a', 'b')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_set_ancestor_keys_should_not_be_overridden(self):
        client = self.server.get_new_client()
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, '.firstName.a', '"some new value"')
        assert self.error_class.is_write_error(str(e.value))

        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, '.age.a', '1')
        assert self.error_class.is_write_error(str(e.value))

        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, '.address.a.b', '"some new value"')
        assert self.error_class.is_write_error(str(e.value))

        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, '.address[0]', '"some new value"')
        assert self.error_class.is_write_error(str(e.value))

    def test_json_set_reject_out_of_boundary_array_index(self):
        client = self.server.get_new_client()
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', wikipedia, 'phoneNumbers[9]', '"123"')
        assert self.error_class.is_outofboundaries_error(str(e.value))

    def test_json_set_insert_value(self):
        client = self.server.get_new_client()
        # insert is allowed if and only if the parent node is the last child in the path.
        for (path, new_val) in [
            ('["address"]["z"]', '"z"'),
            ('.address.z2', '"z2"')
        ]:
            client.execute_command(
                'JSON.SET', wikipedia, path, new_val)
            assert new_val.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

        # if the parent node is not the last child in the path, insertion is not allowed.
        for (path, new_val) in [
            ('["address"]["foo"]["z"]', '"z"'),
            ('.address.foo.z', '"z"')
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.SET', wikipedia, path, new_val)
            assert self.error_class.is_nonexistent_error(str(e.value))

    def test_json_set_negative_array_index(self):
        client = self.server.get_new_client()
        new_val = '"1-2-3"'
        client.execute_command(
            'JSON.SET', wikipedia, '.phoneNumbers[-1].number', new_val)
        assert b'"1-2-3"' == client.execute_command(
            'JSON.GET', wikipedia, '.phoneNumbers[-1].number')
        assert b'["212 555-1234","1-2-3"]' == client.execute_command(
            'JSON.GET', wikipedia, '$.phoneNumbers[*].number')

    def test_json_set_legacy_and_v2path_wildcard(self):
        client = self.server.get_new_client()
        data = '''
                    {"firstName":"John","lastName":"Smith","age":27,"weight":135.17,"isAlive":true,"address":
                    {"street":"21 2nd Street","city":"New York","state":"NY","zipcode":"10021-3100"},
                    "phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"}],
                    "children":[],"spouse":null,"groups":{}}
                    '''
        client.execute_command(
            'JSON.SET', wikipedia2, '.', data)
        client.execute_command(
            'JSON.SET', wikipedia3, '.', data)
        client.execute_command(
            'JSON.SET', wikipedia4, '.', data)

        for (key, path, new_val, exp, path2, exp2) in [
            (wikipedia, '$.address.*',              '"1"',
             b'["1","1","1","1"]', '$.address.*',              b'["1","1","1","1"]'),
            (wikipedia2, '.address.*',               '"1"', b'"1"',
             '$.address.*',              b'["1","1","1","1"]'),
            (wikipedia3, '$.phoneNumbers[*].number', '"1"', b'["1","1"]',
             '$.phoneNumbers[*].number', b'["1","1"]'),
            (wikipedia4, '.phoneNumbers[*].number',  '"1"', b'"1"',
             '$.phoneNumbers[*].number', b'["1","1"]')
        ]:
            client.execute_command(
                'JSON.SET', key, path, new_val)
            assert exp == client.execute_command(
                'JSON.GET', key, path)
            # verify all values
            assert exp2 == client.execute_command(
                'JSON.GET', key, path2)

        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2,3]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2, "c":3}}')

        # NOTE: The expected results below account for the outcome of previous commands.
        test_cases = [
            (k1, '$.a[*]', '1', b'[]',
                b'{"a":[],"b":[1],"c":[1,2,3]}'),
            (k1, '$.b[*]', '2', b'[2]',
             b'{"a":[],"b":[2],"c":[1,2,3]}'),
            (k1, '$.c[*]', '4', b'[4,4,4]',
             b'{"a":[],"b":[2],"c":[4,4,4]}'),
            (k1, '.a[*]', '1',  None,       None),
            (k1, '.b[*]', '3',  b'3',
             b'{"a":[],"b":[3],"c":[4,4,4]}'),
            (k1, '.c[*]', '5',  b'5',
             b'{"a":[],"b":[3],"c":[5,5,5]}'),
            (k2, '$.a.*', '1',  b'[]',
             b'{"a":{},"b":{"a":1},"c":{"a":1,"b":2,"c":3}}'),
            (k2, '$.b.*', '2',
             b'[2]',     b'{"a":{},"b":{"a":2},"c":{"a":1,"b":2,"c":3}}'),
            (k2, '$.c.*', '4',
             b'[4,4,4]', b'{"a":{},"b":{"a":2},"c":{"a":4,"b":4,"c":4}}'),
            (k2, '.a.*', '1',   None,       None),
            (k2, '.b.*', '3',   b'3',
             b'{"a":{},"b":{"a":3},"c":{"a":4,"b":4,"c":4}}'),
            (k2, '.c.*', '5',   b'5',
             b'{"a":{},"b":{"a":3},"c":{"a":5,"b":5,"c":5}}')
        ]

        for (key, path, new_val, exp, exp2) in test_cases:
            if exp == None:
                with pytest.raises(ResponseError) as e:
                    client.execute_command(
                        'JSON.SET', key, path, new_val)
                    client.execute_command(
                        'JSON.GET', key, path)
                assert self.error_class.is_nonexistent_error(str(e.value))
            else:
                client.execute_command(
                    'JSON.SET', key, path, new_val)
                assert exp == client.execute_command(
                    'JSON.GET', key, path)
                # verify entire key
                assert exp2 == client.execute_command(
                    'JSON.GET', key, '.')

    def test_json_get_command_supports_all_datatypes(self):
        client = self.server.get_new_client()
        for (path, value) in [('.firstName', '"John"'),         # string
                              ('.address.city', '"New York"'),  # string
                              ('.spouse', 'null'),              # null
                              ('.children', '[]'),              # empy array
                              ('.groups', '{}'),                # empy object
                              ('.isAlive', 'true'),             # boolean
                              ('.age', '27')]:           # float number
            assert value.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

        for (path, value) in [('["weight"]', '135.17')]:  # float number
            assert value == client.execute_command(
                'JSON.GET', wikipedia, path).decode()

    def test_json_path_syntax_objectkeys(self):
        client = self.server.get_new_client()
        for (path, value) in [('["firstName"]', '"John"'),
                              ('address[\'city\']', '"New York"'),
                              ('[\'address\'][\'city\']', '"New York"'),
                              ('["address"]["city"]', '"New York"'),
                              ('["address"][\'city\']', '"New York"'),
                              ('["isAlive"]', 'true'),
                              ('[\'age\']', '27')]:
            assert value.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

        for (path, value) in [('["weight"]', '135.17')]:
            assert value == client.execute_command(
                'JSON.GET', wikipedia, path).decode()

        test_cases = [
            '[firstName"]',
            'address["city\'',
            '["address\'][[[["city"]',
            '[[["address"]]]["city"]',
            '"["address"][\'city\']',
            '"[\'address"]["city"]',
            '[""""address]["city"]',
            '[address""""]',
            '[\'address\']]][\'city\']',
            '["address"]\'[\'"city"]',
        ]

        # invalid json path
        for path in test_cases:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.GET', wikipedia, path)
            assert self.error_class.is_syntax_error(str(e.value))

    def test_json_get_command_floating_point(self):
        '''
        Test special cases of floating point values.
        '''
        client = self.server.get_new_client()
        for value in ['0', '0.1', '0.3', '1.23456789', '-0.1', '-0.3', '-1.23456789']:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', value)
            assert value == client.execute_command(
                'JSON.GET', wikipedia, '.foo').decode()

        # max double and min double: floating points will be returned exactly as is
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '1.7976e+308')
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.bar', '-1.7976e+308')

        assert '1.7976e+308' == client.execute_command(
            'JSON.GET', wikipedia, '.foo').decode()
        assert '-1.7976e+308' == client.execute_command(
            'JSON.GET', wikipedia, '.bar').decode()

        # 1.234567890123456789 exceeds the precision of a double but will persist regardless.
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '1.234567890123456789')

        assert '1.234567890123456789' == client.execute_command(
            'JSON.GET', wikipedia, '.foo').decode()

        # trailing zeros will no longer be removed.
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '0.3000000')

        assert '0.3000000' == client.execute_command(
            'JSON.GET', wikipedia, '.foo').decode()

    def test_json_get_command_with_multiple_paths(self):
        client = self.server.get_new_client()
        assert b'{".firstName":"John",".lastName":"Smith"}' == client.execute_command(
            'JSON.GET', wikipedia, '.firstName', '.lastName')

        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.GET', wikipedia, '.firstName', '.lastName', '.foo', '.bar')
        assert self.error_class.is_nonexistent_error(str(e.value))

    def test_json_get_command_returns_json_with_default_format(self):
        client = self.server.get_new_client()
        exp = self.data_wikipedia_compact

        # default format is compact JSON string - no indent, no space and no newline
        assert exp == client.execute_command(
            'JSON.GET', wikipedia).decode('utf-8')

        # test NOESCAPE: verify that NOESCAPE is ignored. See the API doc.
        assert exp == client.execute_command(
            'JSON.GET', wikipedia, 'NOESCAPE').decode('utf-8')

    def test_json_get_command_returns_json_with_custom_format(self):
        client = self.server.get_new_client()
        exp = self.data_wikipedia

        # get the root document with custom indent/space/newline
        ret = client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
        assert exp == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')

        # test NOESCAPE: verify that NOESCAPE is ignored. See the API doc.
        assert exp == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE', '\n', 'NOESCAPE').decode('utf-8')
        assert exp == client.execute_command(
            'JSON.GET', wikipedia, 'NOESCAPE', 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
        assert exp == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', 'NOESCAPE', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')

        # get a sub-document with custom indent/space/newline
        exp_json = '{\n\t"street": "21 2nd Street",\n\t"city": "New York",\n\t"state": "NY",\n\t"zipcode": "10021-3100"\n}'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '\t', 'SPACE', ' ', 'NEWLINE', '\n', '.address').decode('utf-8')

        # INDENT: =*=*, SPACE: --, NEWLINE: \r\n
        exp_json = '{\r\n=*=*"street":--"21 2nd Street",\r\n=*=*"city":--"New York",\r\n=*=*"state":--"NY",\r\n=*=*"zipcode":--"10021-3100"\r\n}'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '=*=*', 'SPACE', '--', 'NEWLINE', '\r\n', '.address').decode('utf-8')

        # verify that path args do not need to be positioned at the end
        assert exp == client.execute_command(
            'JSON.GET', wikipedia, '.', 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
        exp_json = '"John"'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', '.firstName', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')

        exp_json = '{\n  ".firstName": "John",\n  ".lastName": "Smith"\n}'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', 'SPACE', ' ', '.firstName', '.lastName', 'NEWLINE', '\n').decode('utf-8')
        exp_json = '{\n\t"street": "21 2nd Street",\n\t"city": "New York",\n\t"state": "NY",\n\t"zipcode": "10021-3100"\n}'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '\t', '.address', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
        exp_json = '[\n\t{\n\t\t"street": "21 2nd Street",\n\t\t"city": "New York",\n\t\t"state": "NY",\n\t\t"zipcode": "10021-3100"\n\t}\n]'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '\t', '$.address', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')

        # check that path args can have formatting in between them
        exp_json = '{\n  ".firstName": "John",\n  ".lastName": "Smith"\n}'
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, 'INDENT', '  ', '.firstName', 'SPACE', ' ', '.lastName', 'NEWLINE', '\n').decode('utf-8')
        assert exp_json == client.execute_command(
            'JSON.GET', wikipedia, '.firstName', 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE', '\n', '.lastName').decode('utf-8')

    def test_json_get_command_returns_json_with_custom_format_error_conditions(self):
        client = self.server.get_new_client()
        # NEWLINE is the last arg
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '  ', 'SPACE', ' ', 'NEWLINE')
        assert self.error_class.is_syntax_error(str(e.value))

        # SPACE is the last arg
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '  ', 'SPACE')
        assert self.error_class.is_syntax_error(str(e.value))

        # INDENT is the last arg
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.GET', wikipedia, 'NEWLINE', '\n', 'SPACE', ' ', 'INDENT')
        assert self.error_class.is_syntax_error(str(e.value))

    def test_json_get_command_with_error_conditions(self):
        client = self.server.get_new_client()
        # if the document key does not exist, the command should return null without throwing an error.
        assert None == client.execute_command(
            'JSON.GET', foo, '.firstName')

        # if the key is not a document key, the command should throw an error.
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.GET', str_key)
        assert self.error_class.is_wrongtype_error(str(e.value))

        # if the JSON path does not exist, the command should throw an error.
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.GET', wikipedia, '.foo')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command('JSON.GET')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_number_as_member_name(self):
        '''
        Given a JSON that has numbers as member names, test GET and SET.
        '''
        client = self.server.get_new_client()
        client.execute_command(
            'JSON.SET', k1, '.', '{"1":1, "2":{"3":4}}')
        client.execute_command('COPY', k1, k2)
        client.execute_command('COPY', k1, k3)
        client.execute_command('COPY', k1, k4)
        client.execute_command('COPY', k1, k5)

        # test GET
        for (path, exp) in [
            ('["2"]["3"]',      '4'),
            ('[\'2\'][\'3\']',  '4'),
            ('.1',              '1'),
            ('.2.3',            '4'),
            ('$.2.3',           '[4]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', k1, path).decode()

        # test SET
        for (key, path, val, exp_new_val) in [
            (k1, '["2"]["3"]',      '5', '{"1":1,"2":{"3":5}}'),
            (k2, '[\'2\'][\'3\']',  '5', '{"1":1,"2":{"3":5}}'),
            (k3, '.2.3',            '5', '{"1":1,"2":{"3":5}}'),
            (k4, '.1',              '2', '{"1":2,"2":{"3":4}}'),
            (k5, '$.2.*',           '5', '{"1":1,"2":{"3":5}}')
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, path, val)
            assert exp_new_val == client.execute_command(
                'JSON.GET', key, '.').decode()

    def test_json_get_legacy_and_v2path_wildcard(self):
        '''
        Test two versions of path syntax - V2 JSONPath and the legacy path. A V2 JSONPath must starts with the dollar sign
        that represents the root element. If a path does not start with the dollar sign, it's a legacy path.

        For queries, the legacy path always returns a single value, which is the first value if multiple values are
        selected. If no value is selected, the legacy path returns NONEXISTENT error. The JSONPath always returns an
        array of values, which could contain zero or one or more values.
        '''
        client = self.server.get_new_client()
        test_cases = [
            ('$.address.*',
             b'["21 2nd Street","New York","NY","10021-3100"]'),
            ('$.[\'address\'].*',
             b'["21 2nd Street","New York","NY","10021-3100"]'),
            ('.address.*',                      b'"21 2nd Street"'),
            ('.["address"].*',                  b'"21 2nd Street"'),
            ('$.phoneNumbers.*.type',           b'["home","office"]'),
            ('$.phoneNumbers[*].type',          b'["home","office"]'),
            ('$.["phoneNumbers"][*].["type"]',  b'["home","office"]'),
            ('.phoneNumbers.*.type',            b'"home"'),
            ('.phoneNumbers[*].type',           b'"home"'),
            ('.["phoneNumbers"][*].["type"]',   b'"home"'),
            ('$.[ \'address\' ].*',
             b'["21 2nd Street","New York","NY","10021-3100"]'),
            ('.[ "address" ].*',                b'"21 2nd Street"'),
            ('$.[ "phoneNumbers" ][ * ].[ "type" ]',  b'["home","office"]'),
        ]

        for (path, exp) in test_cases:
            assert exp == client.execute_command(
                'JSON.GET', wikipedia, path)

        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b": {"a": 1}, "c": {"a": 1, "b": 2}}')
        client.execute_command(
            'JSON.SET', k3, '.', '{"a":[[[1,2],[3,4],[5,6]],[[7,8],[9,10],[11,12]]]}')
        client.execute_command(
            'JSON.SET', k4, '.', '{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h:":1}}}}}}}}')

        # JSONPath always returns an array of values
        # Test multiple wildcards
        for (key, path, exp) in [
            (k1, '$.a[*]', b'[]'),
            (k1, '$.b[*]', b'[1]'),
            (k1, '$.c[*]', b'[1,2]'),
            (k2, '$.a.*',  b'[]'),
            (k2, '$.b.*',  b'[1]'),
            (k2, '$.c.*',  b'[1,2]'),
            (k3, '$.a[*]',  b'[[[1,2],[3,4],[5,6]],[[7,8],[9,10],[11,12]]]'),
            (k3, '$.a[*][*]',  b'[[1,2],[3,4],[5,6],[7,8],[9,10],[11,12]]'),
            (k3, '$.a[*][*][1]',  b'[2,4,6,8,10,12]'),
            (k4, '$.a.*.*',  b'[{"d":{"e":{"f":{"g":{"h:":1}}}}}]'),
            (k4, '$.a.*.*.*',  b'[{"e":{"f":{"g":{"h:":1}}}}]'),
            (k4, '$.a.*.c.*',  b'[{"e":{"f":{"g":{"h:":1}}}}]'),
            (k4, '$.a.*.c.*.e.*',  b'[{"g":{"h:":1}}]'),
            (k4, '$.a.*.c.*.e.*.g.*',  b'[1]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        # Legacy path always returns a single value, which is the first value.
        # Test multiple wildcards
        for (key, path, exp) in [
            (k1, '.b[*]', b'1'),
            (k1, '.c[*]', b'1'),
            (k2, '.b.*',  b'1'),
            (k2, '.c.*',  b'1'),
            (k3, '.a[*]',  b'[[1,2],[3,4],[5,6]]'),
            (k3, '.a[*][*]',  b'[1,2]'),
            (k3, '.a[*][*][1]',  b'2'),
            (k4, '.a.*.*',  b'{"d":{"e":{"f":{"g":{"h:":1}}}}}'),
            (k4, '.a.*.*.*',  b'{"e":{"f":{"g":{"h:":1}}}}'),
            (k4, '.a.*.c.*',  b'{"e":{"f":{"g":{"h:":1}}}}'),
            (k4, '.a.*.c.*.e.*',  b'{"g":{"h:":1}}'),
            (k4, '.a.*.c.*.e.*.g.*',  b'1')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        # Legacy path returns non-existent error if no value is selected.
        for (key, path, exp) in [
            (k1, '.a[*]', None),
            (k2, '.a.*', None)
        ]:
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    'JSON.GET', key, path)
            assert self.error_class.is_nonexistent_error(str(e.value))

    def test_json_get_negative_array_index_legacypath(self):
        '''
        Test negative array index with the legacy path syntax.
        '''
        client = self.server.get_new_client()
        # Negative indices do not throw error in Valkey
        test_cases = [
            ('.phoneNumbers[-2].type',            '"home"'),
            ('.phoneNumbers[-1].type',            '"office"'),
            ('.phoneNumbers[ -1 ].type',          '"office"'),
            ('.["phoneNumbers"][-2]["type"]',     '"home"'),
            ('.["phoneNumbers"][-1]["type"]',     '"office"'),
            ('.["phoneNumbers"][ -1]["type" ]',   '"office"'),
            ('.["phoneNumbers"][-1 ][ "type"]',   '"office"'),
            ('.["phoneNumbers"][ -1 ][ "type" ]', '"office"')
        ]

        # Out of boundary test cases
        oob_test_cases = [
            '.phoneNumbers[2].type',
            '.phoneNumbers[10].type',
            '.phoneNumbers[-3].type',
        ]

        # Legacy path always returns a single value
        for (path, exp) in test_cases:
            assert exp.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

        # index out of bounds
        for path in oob_test_cases:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.GET', wikipedia, path)
            assert self.error_class.is_outofboundaries_error(str(e.value))

        # test using negative index on a non-array value
        for path in [
            '.firstName[-1]',
            '.age[-1]',
            '.weight[-1]',
            '.address[-1]',
            '.phoneNumbers[0][-1]'
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.GET', wikipedia, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

    def test_json_get_negative_array_index_v2path(self):
        '''
        Test negative array index with the V2 JSONPath syntax.
        '''
        client = self.server.get_new_client()
        test_cases = [
            ('$.phoneNumbers[-2].type',         '["home"]'),
            ('$.phoneNumbers[ -2 ].type',       '["home"]'),
            ('$.phoneNumbers[-1].type',         '["office"]'),
            ('$.phoneNumbers[ -1].type',        '["office"]'),
            ('$.phoneNumbers[-1 ].type',        '["office"]'),
            ('$.["phoneNumbers"][-2]["type"]',  '["home"]'),
            ('$.["phoneNumbers"][-1]["type"]',  '["office"]'),
            # index out of bounds
            ('$.phoneNumbers[2].type',          '[]'),
            ('$.phoneNumbers[10].type',         '[]'),
            # using negative index on a non-array value
            ('$.firstName[-1]',                 '[]'),
            ('$.age[-1]',                       '[]'),
            ('$.weight[-1]',                    '[]'),
            ('$.weight[    -1 ]',               '[]'),
            ('$.phoneNumbers[0][-1]',           '[]'),
            ('$.phoneNumbers[ 0][ -1 ]',        '[]'),
            ('$.phoneNumbers[ 0 ][ -1 ]',       '[]'),
            ('$.[ "phoneNumbers" ][ -1 ][ "type" ]',  '["office"]'),
            ('$.phoneNumbers[-3].type',         '[]'),
            ('$.phoneNumbers[ -3 ].type',       '[]'),
        ]

        # JSONPath always returns an array of values
        for (path, exp) in test_cases:
            assert exp.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

    def test_json_get_v2path_array_slice(self):
        '''
        Test negative array slice with the V2 JSONPath syntax.
        '''
        client = self.server.get_new_client()

        test_cases = [
            ('$[0:3]',         '[0,1,2]'),
            ('$[ 0 : 3 ]',     '[0,1,2]'),
            ('$[0:+3]',        '[0,1,2]'),
            ('$[ 0 : +3 ]',    '[0,1,2]'),
            ('$[0:-1]',        '[0,1,2,3,4,5,6,7,8]'),
            ('$[ 0 : -1 ]',    '[0,1,2,3,4,5,6,7,8]'),
            ('$[2:-2]',        '[2,3,4,5,6,7]'),
            ('$[ 2 : -2 ]',    '[2,3,4,5,6,7]'),
            ('$[+2:-2]',       '[2,3,4,5,6,7]'),
            ('$[1:1]',         '[]'),
            ('$[1:2]',         '[1]'),
            ('$[+1:+2]',       '[1]'),
            ('$[1:3]',         '[1,2]'),
            ('$[1:0]',         '[]'),
            ('$[5:]',          '[5,6,7,8,9]'),
            ('$[ 5 :  ]',      '[5,6,7,8,9]'),
            ('$[:3]',          '[0,1,2]'),
            ('$[  :  3]',      '[0,1,2]'),
            ('$[:+3]',         '[0,1,2]'),
            ('$[: +3]',        '[0,1,2]'),
            ('$[:6:2]',        '[0,2,4]'),
            ('$[ : 6 : 2]',    '[0,2,4]'),
            ('$[:]',           '[0,1,2,3,4,5,6,7,8,9]'),
            ('$[ : ]',         '[0,1,2,3,4,5,6,7,8,9]'),
            ('$[::]',          '[0,1,2,3,4,5,6,7,8,9]'),
            ('$[ : : ]',       '[0,1,2,3,4,5,6,7,8,9]'),
            ('$[::2]',         '[0,2,4,6,8]'),
            ('$[ : : 2 ]',     '[0,2,4,6,8]'),
            ('$[3::2]',        '[3,5,7,9]'),
            ('$[3 :: 2]',      '[3,5,7,9]'),
            ('$[0::1]',        '[0,1,2,3,4,5,6,7,8,9]'),
            ('$[0:8:2]',       '[0,2,4,6]'),
            ('$[ 0 : 8 : 2 ]', '[0,2,4,6]'),
            ('$[0:+8:+2]',     '[0,2,4,6]'),
            ('$[0 : +8 : +2]', '[0,2,4,6]'),
            ('$[6:0:-1]',      '[6,5,4,3,2,1]'),
            ('$[6::-1]',       '[]'),
            ('$[6:0:-2]',      '[6,4,2]'),
            ('$[6::-2]',       '[]'),
            ('$[8:0:-2]',      '[8,6,4,2]')
        ]

        client.execute_command(
            'JSON.SET', k1, '.', '[0,1,2,3,4,5,6,7,8,9]')
        for (path, exp) in test_cases:
            assert exp.encode() == client.execute_command('JSON.GET', k1, path)

    def test_json_get_v2path_array_union(self):
        '''
        Test array union with the V2 JSONPath syntax.
        '''
        client = self.server.get_new_client()

        test_cases = [
            ('$[0,1,2]',                             '[0,1,2]'),
            ('$[0, 1, 2]',                           '[0,1,2]'),
            ('$[0, 1, 2  ]',                         '[0,1,2]'),
            ('$[ 0, 1, 2 ]',                         '[0,1,2]'),
            ('$[ 0,1, 2 ]',                          '[0,1,2]'),
            ('$[0,1]',                               '[0,1]'),
            ('$[-1,-2]',                             '[9,8]'),
            ('$[-10,-5,-6]',                         '[0,5,4]'),
            ('$[0,1,5,0,1,2]',                       '[0,1,5,0,1,2]'),
            ('$[0, 1,5,0, 1,2]',                     '[0,1,5,0,1,2]'),
            ('$[ 0, 1, 5, 0, 1, 2 ]',                '[0,1,5,0,1,2]'),
            ('$[ -10 , -5 , -6 ]',                   '[0,5,4]'),
            ('$[-10,-9,-8,0,1,2,-1000,1000]',        '[0,1,2,0,1,2]'),
        ]

        client.execute_command(
            'JSON.SET', k1, '.', '[0,1,2,3,4,5,6,7,8,9]')
        for (path, exp) in test_cases:
            assert exp.encode() == client.execute_command('JSON.GET', k1, path)

        client.execute_command(
            'JSON.SET', k2, '.', '[{"name":"name0","id":0},{"name":"name1","id":1},{"name":"name2","id":2}]')
        for (path, exp) in [
            ('$[0,2].name',         '["name0","name2"]'),
        ]:
            assert exp.encode() == client.execute_command('JSON.GET', k2, path)

        # we do not support mixing of  unions and slices, nor do we support extraneous commas
        for path in [
            '$[0,1,2:4]',
            '$[0:2,3,4]',
            '$[0,,4]',
            '$[0,,,4]',
            '$[,4]',
            '$[4,]',
            '$[,4,]',
            '$[,,4,,]',
            '$[,]',
            '$[,0,4]',
            '$[,0,4,]',
            '$[0,4,]',
        ]:
            with pytest.raises(ResponseError) as e:
                assert None == client.execute_command(
                    'JSON.GET', k1, path)
            assert self.error_class.is_syntax_error(str(e.value))

    def test_json_get_multipaths_legacy_and_v2path_wildcard(self):
        '''
        Test JSON.GET with multiple paths, legacy path or v2 JSONPath or mixed.
        '''
        client = self.server.get_new_client()

        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b": {"a": 1}, "c": {"a": 1, "b": 2}}')

        test_cases = [
            (k1, '.b[*]', '.c[*]', '{".b[*]":1,".c[*]":1}'),
            (k2, '.b.*', '.c.*',   '{".b.*":1,".c.*":1}'),
        ]

        # if all paths are legacy path, the result conforms to the legacy path version
        for (key, path1, path2, exp) in test_cases:
            # 1st path returns 1 value. 2nd path returns the first one of 2 values.
            assert exp.encode() == client.execute_command(
                'JSON.GET', key, path1, path2)

        # all paths are legacy path, the result conforms to the legacy path version.
        # If one path returns 0 value, the command should fail with NONEXISTENT error.
        for (key, path1, path2, exp) in [
            (k1, '.a[*]',   '.b[*]', None),
            (k2, '.a.*',    '.b.*',  None),
            (k2, '.foo.*',  '.c.*',  None)
        ]:
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    'JSON.GET', key, path1, path2)
            assert self.error_class.is_nonexistent_error(str(e.value))

        # if at least one path is JSONPath, the result conforms to the JSONPath version
        for (key, path1, path2, path3, exp) in [
            (k1, '$.a[*]', '$.b[*]', '$.c[*]',
             '{"$.a[*]":[],"$.b[*]":[1],"$.c[*]":[1,2]}'),
            (k1, '$.a[*]', '.b[*]',  '.c[*]',
             '{"$.a[*]":[],".b[*]":[1],".c[*]":[1,2]}'),
            (k1, '.a[*]',  '$.b[*]', '.c[*]',
             '{".a[*]":[],"$.b[*]":[1],".c[*]":[1,2]}'),
            (k2, '.a.*',   '.b.*',   '$.c.*',
             '{".a.*":[],".b.*":[1],"$.c.*":[1,2]}'),
            (k2, '$.a.*',  '$.b.*',  '.c.*',
             '{"$.a.*":[],"$.b.*":[1],".c.*":[1,2]}'),
            (k2, '.a.*',   '$.b.*',  '$.c.*',
             '{".a.*":[],"$.b.*":[1],"$.c.*":[1,2]}'),
            # 1st path returns 0 value. 2nd path returns 1 value. 3rd path returns 2 values.
            (k2, '.foo.*', '$.b.*',  '$.c.*',
             '{".foo.*":[],"$.b.*":[1],"$.c.*":[1,2]}')
        ]:
            assert exp.encode() == client.execute_command(
                'JSON.GET', key, path1, path2, path3)

    def test_json_mget_command(self):
        client = self.server.get_new_client()
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', '{"foo":"bar1"}')
        assert b'OK' == client.execute_command(
            'JSON.SET', k2, '.', '{"foo":"bar2"}')
        assert b'OK' == client.execute_command(
            'JSON.SET', k3, '.', '{"foo":"bar3"}')
        assert [b'"bar1"', b'"bar2"', b'"bar3"'] == client.execute_command(
            'JSON.MGET', k1, k2, k3, '.foo')
        # test the condition of JSON path does not exist
        assert [None, None, None] == client.execute_command(
            'JSON.MGET', k1, k2, k3, '.bar')
        # test the condition of key does not exist
        assert [None, None] == client.execute_command(
            'JSON.MGET', baz, foo, '.')
        assert [None, b'"bar2"'] == client.execute_command(
            'JSON.MGET', baz, k2, '.foo')

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command('JSON.MGET')
        assert str(e.value).find('wrong number of arguments') >= 0

        assert b'OK' == client.execute_command(
            'json.set', k4, '.', '[2,5,{"level0":[null,true,{"level0_1":[3,false]}],"level1":{"level1_0":33}}]')
        assert b'OK' == client.execute_command(
            'json.set', k5, '.', '[4,5,{"level0":[null,false,{"level0_1":[null,false]}],"level1":{"level1_0":[12,13]}}]')
        assert b'OK' == client.execute_command(
            'json.set', k6, '.', '[2,5,{"level0":[true,20,{"level0_1":[3,true]}],"level1":{"level1_0":33}}]')
        for (path, exp) in [
            ('$..level0_1', [b"[[3,false]]",
             b"[[null,false]]", b"[[3,true]]"]),
            ('$.[2].level1', [b'[{"level1_0":33}]',
             b'[{"level1_0":[12,13]}]', b'[{"level1_0":33}]']),
            ('$.[2].level1.level1_0[2]', [b"[]", b"[]", b"[]"]),
            ('$.[2].level1.level1_0[1]', [b"[]", b"[13]", b"[]"])
        ]:
            assert [exp[0], exp[1], exp[2]] == client.execute_command(
                'JSON.MGET', k4, k5, k6, path)

    def test_json_key_declaration(self):
        client = self.server.get_new_client()
        cmd_need_val = set(
            'SET NUMMULTBY NUMINCRBY ARRAPPEND ARRINDEX STRAPPEND RESP'.split())

        # These commands should only get the single key
        for cmd in ('DEL', 'GET', 'SET', 'TYPE', 'NUMINCRBY', 'NUMMULTBY', 'TOGGLE', 'STRAPPEND', 'STRLEN',
                    'ARRAPPEND', 'ARRINDEX', 'ARRLEN', 'ARRPOP', 'CLEAR', 'OBJKEYS',
                    'OBJLEN', 'FORGET', 'RESP'):
            if cmd not in cmd_need_val:
                assert [k1] == client.execute_command(
                    'COMMAND GETKEYS', f'JSON.{cmd}', k1)
            else:
                # Dummy value in command
                assert [k1] == client.execute_command(
                    'COMMAND GETKEYS', f'JSON.{cmd}', k1, '.', '5')

            # ARRINSERT requires index
            assert ['k1'] == client.execute_command(
                'COMMAND GETKEYS', 'JSON.ARRINSERT', 'k1', '.', 0, '5')
            # ARRINSERT requires start end
            assert ['k1'] == client.execute_command(
                'COMMAND GETKEYS', 'JSON.ARRTRIM', 'k1', '.', 0, 5)

        debug_subcmd = set('MEMORY DEPTH'.split())
        for cmd in debug_subcmd:
            assert [k1] == client.execute_command(
                'COMMAND GETKEYS', 'JSON.DEBUG', cmd, k1)

        # JSON.MGET is the only multi-key command, so make sure it returns the right set of keys
        assert [k1, k2, k3] == client.execute_command(
            'COMMAND GETKEYS', 'JSON.MGET', k1, k2, k3, '.')

    def __json_del_or_forget__(self, cmd):
        client = self.server.get_new_client()
        # delete an element
        for path in ['.spouse', '.phoneNumbers']:
            assert 1 == client.execute_command(
                cmd, wikipedia, path)
            with pytest.raises(ResponseError) as e:
                assert None == client.execute_command(
                    'JSON.GET', wikipedia, path)
            assert self.error_class.is_nonexistent_error(str(e.value))

        # delete a doc: path not provided
        assert 1 == client.execute_command(cmd, wikipedia)
        assert 0 == client.execute_command('EXISTS', wikipedia)

        # delete a doc: path arg provided
        client.execute_command('json.set', k1, '.', '1')
        assert 1 == client.execute_command(cmd, k1, '.')
        assert 0 == client.execute_command('EXISTS', k1)

        # return should be 0 if the document key does not exist
        assert 0 == client.execute_command(
            cmd, foo, '.firstName')

        # return should be 0 if the path does not exist
        assert 0 == client.execute_command(
            cmd, wikipedia, '.foo')

        # Wrong number of arguments

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                cmd, wikipedia, '.children', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_del_command(self):
        self.__json_del_or_forget__('JSON.DEL')

    def test_json_forget_command(self):
        self.__json_del_or_forget__('JSON.FORGET')

    def test_json_del_command_v2path_wildcard(self):
        client = self.server.get_new_client()
        client.execute_command(
            'json.set', k1, '.', '{"x": {}, "y": {"a":"a"}, "z": {"a":"", "b":"b"}}')
        client.execute_command(
            'json.set', k2, '.', '[0,1,2,3,4,5,6,7,8,9]')
        client.execute_command(
            'json.set', k3, '.', '[0,1,2,3,4,5,6,7,8,9]')
        client.execute_command(
            'json.set', k4, '.', '[0,1,2,3,4,5,6,7,8,9]')
        client.execute_command(
            'json.set', k5, '.', '[0,1,2,3,4,5,6,7,8,9]')

        # NOTE: The expected values below account for the outcome of previous commands.
        for (key, path, exp_ret, exp_val) in [
            (k1, '$.z.*',   2,  '{"x":{},"y":{"a":"a"},"z":{}}'),
            (k1, '$.*',     3,  '{}'),
            (k2, '$.[3:6]', 3,  '[0,1,2,6,7,8,9]'),
            (k2, '$.*',     7,  '[]'),
        ]:
            assert exp_ret == client.execute_command(
                'JSON.DEL', key, path)
            assert exp_val == client.execute_command(
                'JSON.GET', key).decode()

        # delete whole doc
        for key in [k1, k2]:
            assert 1 == client.execute_command(
                'JSON.DEL', key, '$')
            assert 0 == client.execute_command('EXISTS', key)

        # delete with wildcard, slice, and union
        assert 10 == client.execute_command(
            'JSON.DEL', k3, '$[*]')
        assert b'[]' == client.execute_command('JSON.GET', k3)
        assert 4 == client.execute_command(
            'JSON.DEL', k4, '$[3:7]')
        assert b'[0,1,2,7,8,9]' == client.execute_command(
            'JSON.GET', k4)
        assert 4 == client.execute_command(
            'JSON.DEL', k5, '$[1,5,7,8]')
        assert b'[0,2,3,4,6,9]' == client.execute_command(
            'JSON.GET', k5)

        assert b'OK' == client.execute_command(
            'json.set', k1, '.', '[2,5,{"level0":[null,true,{"level0_1":[3,false]}],"level1":{"level1_0":33}}]')
        for (key, path, exp_ret, exp_val) in [
                (k1, '$[2].level0..level0_1[1]',   1,
                 '[2,5,{"level0":[null,true,{"level0_1":[3]}],"level1":{"level1_0":33}}]'),
                (k1, '$[2].level0..level0_1',      1,
                 '[2,5,{"level0":[null,true,{}],"level1":{"level1_0":33}}]'),
                (k1, '$..level1.level1_0',         1,
                 '[2,5,{"level0":[null,true,{}],"level1":{}}]'),
                (k1, '$..level1',                  1,
                 '[2,5,{"level0":[null,true,{}]}]'),
                (k1, '.*',                         3,  '[]'),
        ]:
            assert exp_ret == client.execute_command(
                'JSON.DEL', key, path)
            assert exp_val == client.execute_command(
                'JSON.GET', key).decode()
            if path != '.*':
                assert '[]' == client.execute_command(
                    'JSON.GET', key, path).decode()
            else:
                with pytest.raises(ResponseError) as e:
                    client.execute_command(
                        'JSON.GET', key, path).decode()
                assert self.error_class.is_nonexistent_error(str(e.value))

    def test_json_unicode_is_supported(self):
        client = self.server.get_new_client()
        for unicode_str in [
            '"Eat, drink, 愛"',
            '"hyvää-élève"'
        ]:
            utf8 = unicode_str.encode('utf-8')
            assert b'OK' == client.execute_command(
                'JSON.SET', k1, '.', unicode_str)
            assert utf8 == client.execute_command(
                'JSON.GET', k1, 'NOESCAPE', '.')
            assert 1 == client.execute_command('JSON.DEL', k1)
            assert 0 == client.execute_command('EXISTS', k1)

    def test_nonASCII_in_jsonpath(self):
        client = self.server.get_new_client()
        client.execute_command(
            'JSON.SET', k1, '.', '{"a": {"愛": "love", "b": "b"}}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"愛": [1,2,3]}')
        for (key, path1, path2, exp) in [
            (k1, '.a.愛', None,   b'"love"'),
            (k1, '.a.愛', '.a.b', b'{".a.\xe6\x84\x9b":"love",".a.b":"b"}'),
            (k2, '.愛[1]', None,  b'2'),
            (k2, '$.愛[*]', None,  b'[1,2,3]')
        ]:
            if path2 is None:
                assert exp == client.execute_command(
                    'JSON.GET', key, path1)
            else:
                # Valkey behavior is weird and returns dictionaries in a non-deterministic order
                assert exp == client.execute_command(
                    'JSON.GET', key, path1, path2)

    def test_json_number_scanner(self):
        '''Test that numeric conversion gets the right types around various edge cases'''
        client = self.server.get_new_client()
        maxpos = (1 << 63) - 1

        for v in [
            (maxpos, b'integer'),
            (-maxpos, b'integer'),
                (-maxpos-1, b'integer')]:
            client.execute_command(
                'JSON.SET', k1, '.', str(v[0]))
            assert v[1] == client.execute_command(
                'JSON.TYPE', k1, '.'), "Value is " + str(v[0])

        for v in [
            (maxpos+1, b'number'),
            (-maxpos-2, b'number')
        ]:

            client.execute_command(
                'JSON.SET', k1, '.', str(v[0]))
            assert v[1] == client.execute_command(
                'JSON.TYPE', k1, '.'), "Value is " + str(v[0])

    def test_json_toggle(self):
        client = self.server.get_new_client()
        # Toggle back and forth
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foobool', 'false')
        assert b'false' == client.execute_command(
            'JSON.GET', wikipedia, '.foobool')

        assert b'true' == client.execute_command(
            'JSON.TOGGLE', wikipedia, '.foobool')
        assert b'true' == client.execute_command(
            'JSON.GET', wikipedia, '.foobool')

        assert b'false' == client.execute_command(
            'JSON.TOGGLE', wikipedia, '.foobool')
        assert b'false' == client.execute_command(
            'JSON.GET', wikipedia, '.foobool')

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.TOGGLE', wikipedia, '.foobool', 'extra')
        assert self.error_class.is_wrong_number_of_arguments_error(
            str(e.value))

        # Wrong types
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foonum', '55')
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.TOGGLE', wikipedia, '.foonum')
        assert self.error_class.is_wrongtype_error(str(e.value))

        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foostr', '"ok"')
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.TOGGLE', wikipedia, '.foostr')
        assert self.error_class.is_wrongtype_error(str(e.value))

    def test_json_toggle_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '{"a":true, "b":false, "c":1, "d":null, "e":"foo", "f":[], "g":{}}')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[true, false, 1, null, "foo", [], {}]')

        for (key, path, exp, exp_new_val) in [
            (k1, '$.*',  [0, 1, None, None, None, None, None],
             '{"a":false,"b":true,"c":1,"d":null,"e":"foo","f":[],"g":{}}'),
            (k1, '$.*',  [1, 0, None, None, None, None, None],
             '{"a":true,"b":false,"c":1,"d":null,"e":"foo","f":[],"g":{}}'),
            (k2, '$[*]', [0, 1, None, None, None, None, None],
             '[false,true,1,null,"foo",[],{}]'),
            (k2, '$[*]', [1, 0, None, None, None, None, None],
             '[true,false,1,null,"foo",[],{}]')
        ]:
            assert exp == client.execute_command(
                'JSON.TOGGLE', key, path)
            assert exp_new_val == client.execute_command(
                'JSON.GET', key, '.').decode()

    def test_json_numincrby(self):
        client = self.server.get_new_client()
        assert b'28' == client.execute_command(
            'JSON.NUMINCRBY', wikipedia, '.age', '1')
        assert b'38' == client.execute_command(
            'JSON.NUMINCRBY', wikipedia, '.age', '10')
        assert b'33' == client.execute_command(
            'JSON.NUMINCRBY', wikipedia, '.age', '-5')

        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '1')
        assert b'1.5' == client.execute_command(
            'JSON.NUMINCRBY', wikipedia, '.foo', '0.5')
        assert b'2' == client.execute_command(
            'JSON.NUMINCRBY', wikipedia, '.foo', '0.5')

        # error condition: document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.NUMINCRBY', foo, '.age', '2')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.NUMINCRBY', wikipedia, '.bar', '2')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.NUMINCRBY', wikipedia, '.age', '1', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_nummultby(self):
        client = self.server.get_new_client()
        assert b'270' == client.execute_command(
            'JSON.NUMMULTBY', wikipedia, '.age', '10')
        assert b'2700' == client.execute_command(
            'JSON.NUMMULTBY', wikipedia, '.age', '10')
        assert b'27' == client.execute_command(
            'JSON.NUMMULTBY', wikipedia, '.age', '0.01')

        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '1')
        assert b'0.5' == client.execute_command(
            'JSON.NUMMULTBY', wikipedia, '.foo', '0.5')
        assert b'0.25' == client.execute_command(
            'JSON.NUMMULTBY', wikipedia, '.foo', '0.5')
        assert b'1' == client.execute_command(
            'JSON.NUMMULTBY', wikipedia, '.foo', '4')

        # error condition: document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.NUMMULTBY', foo, '.age', '2')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.NUMMULTBY', wikipedia, '.bar', '2')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.NUMMULTBY', wikipedia, '.age', '2', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_simple_operations_remove_double_styling(self):
        # Multiplying by one or adding zero will now remove styling
        # and really change output for negative exponents due to decimals
        client = self.server.get_new_client()
        data = [
            ('2.50000', '2.5'),
            ('2e30', '2e+30'),
            ('2E+30', '2e+30'),
            ('2E30', '2e+30'),
            ('2E-30', '2.0000000000000002e-30'),
            ('2e5', '200000'),
            ('-2.50000', '-2.5'),
            ('-2e30', '-2e+30'),
            ('-2E+30', '-2e+30'),
            ('-2E30', '-2e+30'),
            ('-2E-30', '-2.0000000000000002e-30'),
            ('-2e5', '-200000'),
        ]

        for (initial, unstyled) in data:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', initial)
            assert initial == client.execute_command(
                'JSON.GET', wikipedia, '.foo').decode()
            client.execute_command(
                'JSON.NUMMULTBY', wikipedia, '.foo', '1')
            assert unstyled == client.execute_command(
                'JSON.GET', wikipedia, '.foo').decode()

        for (initial, unstyled) in data:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', initial)
            assert initial == client.execute_command(
                'JSON.GET', wikipedia, '.foo').decode()
            client.execute_command(
                'JSON.NUMINCRBY', wikipedia, '.foo', '0')
            assert unstyled == client.execute_command(
                'JSON.GET', wikipedia, '.foo').decode()

    def test_json_double_operations_wrongtype(self):
        client = self.server.get_new_client()
        # None of these will these will succeed because the doubles are not valid or the field is not a double
        for (cmd, key, field, arg) in [
            ('JSON.NUMMULTBY', wikipedia, '.age', '"2.0"'),
            ('JSON.NUMMULTBY', wikipedia, '.age', '2.'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0.'),
            ('JSON.NUMMULTBY', wikipedia, '.age', '-2.'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '-2.0.'),
            ('JSON.NUMMULTBY', wikipedia, '.age', '+2.'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '+2.0.'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '.2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', 'a2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '-a2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '+a2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', 'e2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '-e2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '+e2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', 'e+2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '-e-2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '+E+2.0'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0e'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0eq'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0e3q'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0e+'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0e+a'),
            ('JSON.NUMINCRBY', wikipedia, '.age', '2.0e+41a'),
            ('JSON.NUMINCRBY', wikipedia, '.firstName', '2'),
        ]:
            with pytest.raises(ResponseError) as e:
                assert None == client.execute_command(
                    cmd, key, field, arg)
            assert self.error_class.is_wrongtype_error(str(e.value))

    def test_json_double_consistency(self):
        '''
        Test that double values remain consistent when going through JSON Engine.
            This tests a tolerance of 2^-50 for a decent number of iterations,
            but is not enough to guarantee that level of presicion to our customers.
        Also verify that regular and pretty print double values have the same output.
        '''
        client = self.server.get_new_client()
        # arbitrarily generated hex to double to string, to send to json
        random.seed(1234)
        data = []
        for i in range(24000):
            potential_double = struct.unpack(
                '<d', bytes.fromhex('%016x' % random.randrange(16**16)))[0]
            # We don't want inf/-inf, NaN, or denormalized numbers
            if not (isinf(potential_double) or isnan(potential_double) or frexp(potential_double)[1] == 0):
                data.append(str(potential_double))

        for val in data:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', val)
            v0 = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            vp = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            # verify pretty print content is the same as standard content
            assert v0 is not None and vp is not None and v0 == vp
            # verify content is accurate, worked up to 2^-50 when float was calculated, should work when stored as string
            assert v0.decode() == val

    def test_json_numincrby_large_numbers(self):
        '''
        Test edge cases with large values near the boundaries of int64 and double.
        '''
        # Although the result exceeds the range of int64, it is still a valid double number.
        client = self.server.get_new_client()
        for (val, incr) in [
            ('9223372036854775807', 1),
            ('9223372036854775807', 2),
            ('-9223372036854775808', -1),
            ('-9223372036854775808', -2),
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', val)
            assert val.encode() == client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            # verify pretty-print gives us the same result
            assert val.encode() == client.execute_command(
                'JSON.GET', wikipedia, 'SPACE', ' ', 'NEWLINE', '\n', '.foo')

        # The result is still within the range of int64
        data = [
            ('-9223372036854775808', '-9223372036854775808', 0,
             '-9.2233720368547758e+18', '-9223372036854775808'),
            ('-9223372036854775808', '-9223372036854775808', 1,
             '-9.2233720368547758e+18', '-9223372036854775807'),
            ('-9223372036854775808', '-9223372036854775808', 2,
             '-9.2233720368547758e+18', '-9223372036854775806'),
            ('1.79e+308', '1.79e308', 0, '1.79e+308', '1.79e308'),
            ('1.79e+308', '1.79e308', 1, '1.79e+308', '1.79e308'),
            ('1.79e+308', '1.79e308', -1, '1.79e+308', '1.79e308'),
            ('-1.79e+308', '-1.79e308', 0, '-1.79e+308', '-1.79e308'),
            ('-1.79e+308', '-1.79e308', 1, '-1.79e+308', '-1.79e308'),
            ('-1.79e+308', '-1.79e308', -1, '-1.79e+308', '-1.79e308'),
            ('9223372036854775807', '9223372036854775807', 0,
             '9.2233720368547758e+18', '9223372036854775807'),
            ('9223372036854775807', '9223372036854775807', -1,
             '9.2233720368547758e+18', '9223372036854775806'),
            ('9223372036854775807', '9223372036854775807', -2,
             '9.2233720368547758e+18', '9223372036854775805')
        ]
        iteration_tracker = 0
        for (val, val_alt, incr, exp, exp_alt) in data:
            logging.debug("1058: Iteration %d", iteration_tracker)
            iteration_tracker += 1
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', val)
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and v.decode() == val or v.decode() == val_alt
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            assert v is not None and v.decode() == val or v.decode() == val_alt
            v = client.execute_command(
                'JSON.NUMINCRBY', wikipedia, '.foo', incr)
            assert v is not None and v.decode() == exp or v.decode() == exp_alt
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and v.decode() == exp or v.decode() == exp_alt
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            assert v is not None and v.decode() == exp or v.decode() == exp_alt

        # The result is still within the range of double
        for (val, val_alt, incr, exp, exp_alt) in [
            ('1.79e+308', '1.79e308', 0, '1.79e+308', '1.79e308'),
            ('1.79e+308', '1.79e308', 1, '1.79e+308', '1.79e308'),
            ('1.79e+308', '1.79e308', -1, '1.79e+308', '1.79e308'),
            ('-1.79e+308', '-1.79e308', 0, '-1.79e+308', '-1.79e308'),
            ('-1.79e+308', '-1.79e308', 1, '-1.79e+308', '-1.79e308'),
            ('-1.79e+308', '-1.79e308', -1, '-1.79e+308', '-1.79e308')
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', val)
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and v.decode() == val or v.decode() == val_alt
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            assert v is not None and v.decode() == val or v.decode() == val_alt
            v = client.execute_command(
                'JSON.NUMINCRBY', wikipedia, '.foo', incr)
            assert v is not None and v.decode() == exp or v.decode() == exp_alt
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and v.decode() == exp or v.decode() == exp_alt
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            assert v is not None and v.decode() == exp or v.decode() == exp_alt

    def test_json_nummultby_large_numbers(self):
        '''
        Test edge cases with large values near the boundaries of int64 and double.
        '''
        client = self.server.get_new_client()

        # Multiplication causes overflow
        for (val, val_alt, mult) in [
            ('9223372036854775807', '9223372036854775807', 1.79e+308),
            ('9223372036854775807', '9223372036854775807', -1.79e+308),
            ('1.79e+308', '1.79e308', 1.79e308),
            ('1.03e+300', '1.03e300', 1.03e300),
            ('1.79e+308', '1.79e308', -1.79e308),
            ('-1.03e+300', '-1.03e300', 1.03e300)
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', val)
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and v.decode() == val or v.decode() == val_alt
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'SPACE', ' ', 'NEWLINE', '\n', '.foo')
            assert v is not None and v.decode() == val or v.decode() == val_alt
            with pytest.raises(ResponseError) as e:
                assert None == client.execute_command(
                    'JSON.NUMMULTBY', wikipedia, '.foo', mult)
            assert self.error_class.is_number_overflow_error(str(e.value))

        # The result is still within the range of double
        data = [
            ('9223372036854775807', '9223372036854775807', 0, '0',           '0'),
            ('9223372036854775807', '9223372036854775807', 2,
             '1.8446744073709552e+19', '18446744073709553000.0'),
            ('9223372036854775807', '9223372036854775807', 9223372036854775807,
             '8.5070591730234616e+37', '8.5070591730234616e37'),
            ('9223372036854775807', '9223372036854775807', -9223372036854775807,
             '-8.5070591730234616e+37', '-8.5070591730234616e37'),
            ('1.79e308', '1.79e+308', 0,   '0',          '0'),
            ('1.79e308', '1.79e+308', 1,   '1.79e+308',  '1.79e308'),
            ('-1.79e308', '-1.79e+308', 1, '-1.79e+308', '-1.79e308'),
            ('9223372036854775807', '9223372036854775807', 1,
             '9.2233720368547758e+18', '9223372036854775807')
        ]

        for (val, val_alt, mult, exp, exp_alt) in data:
            assert b'OK' == client.execute_command(
                'JSON.SET', wikipedia, '.foo', val)
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and (
                v.decode() == val or v.decode() == val_alt)
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            assert v is not None and (
                v.decode() == val or v.decode() == val_alt)
            v = client.execute_command(
                'JSON.NUMMULTBY', wikipedia, '.foo', mult)
            # logging.debug("DEBUG val: %s, mult: %f, v: %s, exp: %s" %(val, mult, v.decode(), exp))
            assert v is not None and v.decode() == exp or v.decode() == exp_alt
            v = client.execute_command(
                'JSON.GET', wikipedia, '.foo')
            assert v is not None and v.decode() == exp or v.decode() == exp_alt
            # verify pretty-print gives us the same result
            v = client.execute_command(
                'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
            assert v is not None and v.decode() == exp or v.decode() == exp_alt

    def test_json_strlen_command(self):
        client = self.server.get_new_client()
        assert 2 == client.execute_command(
            'JSON.STRLEN', wikipedia, '.address.state')
        assert 4 == client.execute_command(
            'JSON.STRLEN', wikipedia, '.firstName')

        # edge case: empty string
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '""')
        assert 0 == client.execute_command(
            'JSON.STRLEN', wikipedia, '.foo')

        # return should be null if document key does not exist
        assert None == client.execute_command(
            'JSON.STRLEN', foo, '.firstName')

        # return error if path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.STRLEN', wikipedia, '.bar')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: element is not a string
        for path in ['.address', '.groups', '.age', '.isAlive', '.spouse']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.STRLEN', wikipedia, path)

        # Wrong number of arguments

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.STRLEN', wikipedia, '.firstName', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_strappend_command(self):
        client = self.server.get_new_client()
        for (val, new_len, new_val) in [
            ('"son"',       7,  '"Johnson"'),
            ('" Junior"',   14, '"Johnson Junior"'),
            ('" is"',       17, '"Johnson Junior is"'),
            ('" my"',       20, '"Johnson Junior is my"'),
            ('" friend."',  28, '"Johnson Junior is my friend."'),
            ('""',          28, '"Johnson Junior is my friend."')
        ]:
            assert new_len == client.execute_command(
                'JSON.STRAPPEND', wikipedia, '.firstName', val)
            assert new_val == client.execute_command(
                'JSON.GET', wikipedia, '.firstName').decode('utf-8')

        # edge case: appending to an empty string
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foo', '""')
        client.execute_command(
            'JSON.STRAPPEND', wikipedia, '.foo', '"abc"')
        assert b'"abc"' == client.execute_command(
            'JSON.GET', wikipedia, '.foo')

        # error condition: document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.STRAPPEND', foo, '.firstName', '"abc"')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.STRAPPEND', wikipedia, '.bar', '"abc"')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: attempt to append a non-string value
        for val in ['123', 'true', 'false', 'null', '{}', '[]']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.STRAPPEND', wikipedia, '.firstName', val)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # error condition: attempt to append to a non-string element
        for path in ['.address', '.groups', '.age', '.isAlive', '.spouse']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.STRAPPEND', wikipedia, path, '"12"')
            assert self.error_class.is_wrongtype_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.STRAPPEND', wikipedia, '.firstName', '"abc"', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_strlen_command_legacy_and_jsonpath_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command('JSON.SET', k1, '.',
                               '{"a":{"a":"a"}, "b":{"a":""}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":3}}')

        for (key, path, exp) in [
            (k1, '$.a.a', [1]),
            (k1, '$.a.*', [1]),
            (k1, '$.b.a', [0]),
            (k1, '$.b.*', [0]),
            (k1, '$.c.*', [1, 2]),
            (k1, '$.c.b', [2]),
            (k1, '$.d.*', [None, 1, None]),
            (k1, '.a.a',  1),
            (k1, '.a.*',  1),
            (k1, '.b.a',  0),
            (k1, '.b.*',  0),
            (k1, '.c.*',  1),
            (k1, '.c.b',  2),
        ]:
            assert exp == client.execute_command(
                'JSON.STRLEN', key, path)

        assert 1 == client.execute_command(
            'JSON.STRLEN', k1, '.d.*')

    def test_json_strappend_command_legacy_and_jsonpath_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command('JSON.SET', k1, '.',
                               '{"a":{"a":"a"}, "b":{"a":""}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":3}}')

        # NOTE: The expected result below accounts for the outcome of previous commands.
        for (key, path, append, exp_ret, exp_new_str, exp_whole_json) in [
            (k1, '$.a.a', '"x"', [
             2],   '["ax"]',   '{"a":{"a":"ax"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.a.a', '""',  [
             2],   '["ax"]',   '{"a":{"a":"ax"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.a.*', '"yz"', [4],   '["axyz"]',
             '{"a":{"a":"axyz"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '.a.a',  '"a"', 5,     '"axyza"',
             '{"a":{"a":"axyza"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '.a.*',  '"a"', 6,     '"axyzaa"',
             '{"a":{"a":"axyzaa"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.b.a', '"a"', [
             1],   '["a"]',    '{"a":{"a":"axyzaa"},"b":{"a":"a"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.b.*', '""',  [1],   '["a"]',
             '{"a":{"a":"axyzaa"},"b":{"a":"a"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.b.*', '"a"', [2],   '["aa"]',
             '{"a":{"a":"axyzaa"},"b":{"a":"aa"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '.b.a',  '"a"', 3,     '"aaa"',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaa"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '.b.*',  '"a"', 4,     '"aaaa"',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.c.*', '"a"', [2, 3], '["aa","bba"]',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aa","b":"bba"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.c.b', '"a"', [
             4],   '["bbaa"]', '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aa","b":"bbaa"},"d":{"a":1,"b":"b","c":3}}'),

            # The following strappend changes value at $.c value to {"a":"aaa", "b":"bbaaa"}.
            # strappend returns length of the last updated value, which is 5,
            # while 'json.get .c.*' returns the first selected element, which is "aaa".
            (k1, '.c.*',  '"a"', 5,      '"aaa"',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaa"},"d":{"a":1,"b":"b","c":3}}'),

            (k1, '.c.b',  '"a"', 6,      '"bbaaaa"',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaaa"},"d":{"a":1,"b":"b","c":3}}'),
            (k1, '$.d.*', '"a"', [None, 2, None], '[1,"ba",3]',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaaa"},"d":{"a":1,"b":"ba","c":3}}'),

            # strappend returns length of the last updated value, which is 3 ("baa"),
            # while 'json.get .d.*' returns the first selected element, which is 1.
            (k1, '.d.*',  '"a"', 3,      '1',
             '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaaa"},"d":{"a":1,"b":"baa","c":3}}')
        ]:
            assert exp_ret == client.execute_command(
                'JSON.STRAPPEND', key, path, append)
            assert exp_new_str == client.execute_command(
                'JSON.GET', key, path).decode()

    def test_json_objectlen_command(self):
        client = self.server.get_new_client()
        assert 4 == client.execute_command(
            'JSON.OBJLEN', wikipedia, '.address')

        # edge case: empty object
        assert 0 == client.execute_command(
            'JSON.OBJLEN', wikipedia, '.groups')

        # return should be null if document key does not exist
        assert None == client.execute_command(
            'JSON.OBJLEN', foo, '.address')

        # return error if path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.OBJLEN', wikipedia, '.foo')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an object
        for path in ['.children', '.phoneNumbers', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.OBJLEN', wikipedia, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.OBJLEN', wikipedia, '.', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_objlen_command_jsonpath_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command('JSON.SET', k1, '.',
                               '{"a":{}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":{"a":3,"b":4}}, "e":1}')

        test_cases = [
            (k1, '$.a',   [0]),
            (k1, '$.a.*', []),
            (k1, '.a',    0),
            (k1, '$.c',   [2]),
            (k1, '$.c.*', [None, None]),
            (k1, '.c',    2),
            (k1, '$.d',   [3]),
            (k1, '$.d.*', [None, None, 2]),
            (k1, '.d',    3),
            (k1, '$.*',   [0, 2, 3, None]),
            (k1, '.*',    0),
            (k1, '.d.*',  2),
        ]

        for (key, path, exp) in test_cases:
            assert exp == client.execute_command(
                'JSON.OBJLEN', key, path)

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.OBJLEN', k1, '.a.*')
        assert self.error_class.is_nonexistent_error(str(e.value))

        for (key, path) in [
            (k1, '.c.*'),
            (k1, '.e')
        ]:
            with pytest.raises(ResponseError) as e:
                assert None == client.execute_command(
                    'JSON.OBJLEN', key, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

    def test_json_objectkeys_command(self):
        client = self.server.get_new_client()
        obj_keys = [b'street', b'city', b'state', b'zipcode']
        assert obj_keys == client.execute_command(
            'JSON.OBJKEYS', wikipedia, '.address')

        # edge case: empty object
        assert [] == client.execute_command(
            'JSON.OBJKEYS', wikipedia, '.groups')

        # return should be null if document key does not exist
        assert None == client.execute_command(
            'JSON.OBJKEYS', foo, '.address')

        # return should be null if path does not exist
        assert None == client.execute_command(
            'JSON.OBJKEYS', wikipedia, '.foo')

        # error condition: the element is not an object
        for path in ['.children', '.phoneNumbers', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.OBJKEYS', wikipedia, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.OBJKEYS', wikipedia, '.', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_objkeys_command_jsonpath_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command('JSON.SET', k1, '.',
                               '{"a":{}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":{"a":3,"b":4}}, "e":1}')

        test_cases = [
            (k1, '$.a',   [[]]),
            (k1, '$.a.*', []),
            (k1, '.a',    []),
            (k1, '$.c',   [[b"a", b"b"]]),
            (k1, '.c',    [b"a", b"b"]),
            (k1, '$.d',   [[b"a", b"b", b"c"]]),
            (k1, '$.d.*', [[], [], [b"a", b"b"]]),
            (k1, '.d',    [b"a", b"b", b"c"]),
            (k1, '.d.*',  [b"a", b"b"]),
            (k1, '$.*',   [[], [b"a", b"b"], [b"a", b"b", b"c"], []]),
            (k1, '.*',    [b"a", b"b"]),
            (k1, '$.c.*', [[], []]),
            (k1, '$.c.*', [None, None])
        ]

        for (key, path, exp) in [
        ]:
            assert exp == client.execute_command(
                'JSON.OBJKEYS', key, path)

        for (key, path) in [
            (k1, '.c.*'),
            (k1, '.e')
        ]:
            with pytest.raises(ResponseError) as e:
                assert None == client.execute_command(
                    'JSON.OBJLEN', key, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

    def test_json_arrlen_command(self):
        client = self.server.get_new_client()
        assert 2 == client.execute_command(
            'JSON.ARRLEN', wikipedia, '.phoneNumbers')

        # edge case: empty array
        assert 0 == client.execute_command(
            'JSON.ARRLEN', wikipedia, '.children')

        # return should be null if document key does not exist
        assert None == client.execute_command(
            'JSON.ARRLEN', foo, '.phoneNumbers')

        # return error if path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRLEN', wikipedia, '.foo')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an array
        for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.ARRLEN', wikipedia, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.ARRLEN', wikipedia, '.phoneNumbers', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_arrlen_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[[], \"a\", [\"a\", \"b\"], [\"a\", \"b\", \"c\"], 4]')

        for (key, path, exp) in [
            (k1, '$.[*]', [0, 1, 2, 3]),
            (k2, '$.[*]', [0, None, 2, 3, None])
        ]:
            assert exp == client.execute_command(
                'JSON.ARRLEN', key, path)

    def test_json_arrappend_command(self):
        client = self.server.get_new_client()
        # edge case: append to an empty array
        assert 1 == client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.children', '"John"')
        assert b'["John"]' == client.execute_command(
            'JSON.GET', wikipedia, '.children')

        # append to non-empty array
        assert 3 == client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.children', '"Mary"', '"Tom"')
        assert b'["John","Mary","Tom"]' == client.execute_command(
            'JSON.GET', wikipedia, '.children')
        assert 3 == client.execute_command(
            'JSON.ARRLEN', wikipedia, '.children')

        # return error if document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRAPPEND', foo, '.children', '"Mary"')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRAPPEND', wikipedia, '.foo', '"abc"')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an array
        for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.ARRAPPEND', wikipedia, path, '123')
            assert self.error_class.is_wrongtype_error(str(e.value))

    def test_json_arrappend_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')

        for (key, path, val, exp, new_val) in [
            (k1, '$.[*]', '"a"', [1, 2, 3, 4],
             '[[\"a\"],[\"a\",\"a\"],[\"a\",\"b\",\"a\"],[\"a\",\"b\",\"c\",\"a\"]]'),
            (k2, '$.[*]', '""',  [1, 2, 3, 4],
             '[[\"\"],[\"a\",\"\"],[\"a\",\"b\",\"\"],[\"a\",\"b\",\"c\",\"\"]]')
        ]:
            assert exp == client.execute_command(
                'JSON.ARRAPPEND', key, path, val)
            assert new_val == client.execute_command(
                'JSON.GET', key, path).decode()

    def test_json_arrpop_command(self):
        client = self.server.get_new_client()
        # edge case: pop an empty array
        assert None == client.execute_command(
            'JSON.ARRPOP', wikipedia, '.children')

        # populate the array
        assert 3 == client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')

        for (idx, popped_out, new_len, new_val) in [
            (1, '"Mary"', 2, '["John","Tom"]'),
            (-1, '"Tom"', 1, '["John"]'),
            (0, '"John"', 0, '[]')
        ]:
            assert popped_out == client.execute_command(
                'JSON.ARRPOP', wikipedia, '.children', idx).decode('utf-8')
            assert new_len == client.execute_command(
                'JSON.ARRLEN', wikipedia, '.children')
            assert new_val == client.execute_command(
                'JSON.GET', wikipedia, '.children').decode('utf-8')

        # edge case: pop an empty array
        assert None == client.execute_command(
            'JSON.ARRPOP', wikipedia, '.children')

        # return error if document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRPOP', foo, '.children')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRPOP', wikipedia, '.foo')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an array
        for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.ARRPOP', wikipedia, path)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # test large index: larger than int32
        client.execute_command(
            'JSON.ARRPOP', wikipedia, ".children", 3000000000)

        # Wrong number of arguments
        res = b'{"type":"home","number":"212 555-1234"}'
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.ARRPOP', wikipedia, '.phoneNumbers', 0, 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_arrpop_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '[[], ["a"], ["a", "b"], ["a", "b", "c"]]')

        client.execute_command('COPY', k1, k2)
        client.execute_command('COPY', k1, k3)

        for (key, path, index, exp, exp_new_val) in [
            (k1, '$.[*]', 0,  [None, b'"a"', b'"a"', b'"a"'],
             '[[],[],["b"],["b","c"]]'),
            (k2, '$.[*]', 1,  [None, b'"a"', b'"b"', b'"b"'],
             '[[],[],["a"],["a","c"]]'),
            (k3, '$.[*]', -1, [None, b'"a"', b'"b"', b'"c"'],
             '[[],[],["a"],["a","b"]]')
        ]:
            assert exp == client.execute_command(
                'JSON.ARRPOP', key, path, index)
            assert exp_new_val == client.execute_command(
                'JSON.GET', key, '.').decode()

    def test_json_arrinsert_command(self):
        client = self.server.get_new_client()
        # edge case: insert into an empty array
        assert 1 == client.execute_command(
            'JSON.ARRINSERT', wikipedia, '.children', 0, '"foo"')
        assert b'["foo"]' == client.execute_command(
            'JSON.GET', wikipedia, '.children')
        assert b'"foo"' == client.execute_command(
            'JSON.ARRPOP', wikipedia, '.children')

        # populate the array
        assert 3 == client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
        assert b'["John","Mary","Tom"]' == client.execute_command(
            'JSON.GET', wikipedia, '.children')

        for (idx, val, new_len, new_val) in [
            (0, '"Kathy"', 4, '["Kathy","John","Mary","Tom"]'),
            (2, '"Rose"', 5, '["Kathy","John","Rose","Mary","Tom"]'),
            (3, '"Bob"', 6, '["Kathy","John","Rose","Bob","Mary","Tom"]'),
            (-1, '"Peter"', 7,
             '["Kathy","John","Rose","Bob","Mary","Peter","Tom"]'),
            (-1, '"Jane"', 8,
             '["Kathy","John","Rose","Bob","Mary","Peter","Jane","Tom"]'),
            (8, '"Grace"', 9,
             '["Kathy","John","Rose","Bob","Mary","Peter","Jane","Tom","Grace"]')
        ]:
            assert new_len == client.execute_command(
                'JSON.ARRINSERT', wikipedia, '.children', idx, val)
            assert new_val == client.execute_command(
                'JSON.GET', wikipedia, '.children').decode('utf-8')

        # return error if document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINSERT', foo, '.children', 0, '"abc"')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINSERT', wikipedia, '.foo', 0, '123')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an array
        for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.ARRINSERT', wikipedia, path, 0, '1')
            assert self.error_class.is_wrongtype_error(str(e.value))

        # error condition: index arg is out of array boundaries
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINSERT', wikipedia, ".children", 3000000000, '"a"')
        assert self.error_class.is_outofboundaries_error(str(e.value))

        # error condition: index arg is out of array boundaries
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINSERT', wikipedia, ".children", 31, '"a"')
        assert self.error_class.is_outofboundaries_error(str(e.value))

    def test_json_arrinsert_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '[[], [0], [0, 1], [0, 1, 2]]')
        # COPY NOT SUPPORTED by REJSON
        client.execute_command('JSON.SET', k2, '.',
                               '[[], [0], [0, 1], [0, 1, 2]]')
        client.execute_command('JSON.SET', k3, '.',
                               '[[], [0], [0, 1], [0, 1, 2]]')

        test_cases = [
            (k1, '$.[*]', 0,  '3', [1, 2, 3, 4],
             '[[3],[3,0],[3,0,1],[3,0,1,2]]'),
            (k3, '$.[*]', -1, '3', [1, 2, 3, 4],
             '[[3],[3,0],[0,3,1],[0,1,3,2]]')
        ]

        # Negative paths beyond array length work strangely in ReJSON
        for (key, path, index, val, exp, exp_new_val) in test_cases:
            assert exp == client.execute_command(
                'JSON.ARRINSERT', key, path, index, val)
            assert exp_new_val == client.execute_command(
                'JSON.GET', key, '.').decode()

        # test index out of bounds error
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINSERT', k2, "$.[*]", 1, '3')
        assert self.error_class.is_outofboundaries_error(str(e.value))

    def test_json_clear_command(self):
        client = self.server.get_new_client()

        # populate the array to be cleared
        assert 3 == client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
        assert b'["John","Mary","Tom"]' == client.execute_command(
            'JSON.GET', wikipedia, '.children')

        # clears and counts 1, 0 remain
        assert 1 == client.execute_command(
            'JSON.CLEAR', wikipedia, '.children')
        assert 0 == client.execute_command(
            'JSON.ARRLEN', wikipedia, '.children')

        # clears empty array
        assert 0 == client.execute_command(
            'JSON.CLEAR', wikipedia, '.children')
        assert 0 == client.execute_command(
            'JSON.ARRLEN', wikipedia, '.children')

        # if path does not exist, the command should return 0
        assert 0 == client.execute_command(
            'JSON.CLEAR', wikipedia, '.foo')

        # if the value at the path is not a container, the command should return 0
        assert b'OK' == client.execute_command(
            'JSON.SET', wikipedia, '.foobool', 'false')
        assert 0 == client.execute_command(
            'JSON.CLEAR', wikipedia, '.foobool')

        # clear the wikipedia object entirely
        wikipedia_objlen = client.execute_command(
            'JSON.OBJLEN', wikipedia)
        assert 0 != wikipedia_objlen
        assert 1 == client.execute_command(
            'JSON.CLEAR', wikipedia)
        assert 0 == client.execute_command(
            'JSON.OBJLEN', wikipedia)

    def test_json_clear_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '{"a":{}, "b":{"a": 1, "b": null, "c": true}, "c":1, "d":true, "e":null, "f":"d"}')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[[], [0], [0,1], [0,1,2], 1, true, null, "d"]')

        test_cases = [
            (k1, '$.*',  4, '{"a":{},"b":{},"c":0,"d":false,"e":null,"f":""}'),
            (k2, '$[*]', 6, '[[],[],[],[],0,false,null,""]')
        ]

        for (key, path, exp, exp_new_val) in test_cases:
            assert exp == client.execute_command(
                'JSON.CLEAR', key, path)
            assert exp_new_val == client.execute_command(
                'JSON.GET', key, '.').decode()

    def test_json_arrtrim_command(self):
        client = self.server.get_new_client()
        # edge case: empty array
        assert 0 == client.execute_command(
            'JSON.ARRTRIM', wikipedia, '.children', 0, 1)

        # populate the array
        assert 3 == client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
        assert b'["John","Mary","Tom"]' == client.execute_command(
            'JSON.GET', wikipedia, '.children')

        for (path, start, end, new_len, new_val) in [
            ('.children',       1, 2, 2, '["Mary","Tom"]'),
            ('.children',       0, 0, 1, '["Mary"]'),
            ('.children',      -1, 5, 1, '["Mary"]'),
            ('.phoneNumbers',   2, 0, 0, '[]')
        ]:
            assert new_len == client.execute_command(
                'JSON.ARRTRIM', wikipedia, path, start, end)
            assert new_val == client.execute_command(
                'JSON.GET', wikipedia, path).decode('utf-8')

        # return error if document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRTRIM', foo, '.children', 0, 1)
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRTRIM', wikipedia, '.foo', 0, 3)
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an array
        for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.ARRTRIM', wikipedia, path, 0, 1)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # test large index: larger than int32
        client.execute_command(
            'JSON.ARRTRIM', wikipedia, ".phoneNumbers", 3000000000, 3000000001)

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.ARRTRIM', wikipedia, '.phoneNumbers', 0, 1, 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_arrtrim_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '[[], ["a"], ["a", "b"], ["a", "b", "c"]]')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[[], ["a"], ["a", "b"], ["a", "b", "c"]]')
        assert b'OK' == client.execute_command('JSON.SET', k3, '.',
                                               '[[], [0], [0,1], [0,1,2], [0,1,2,3]]')

        for (key, path, start, stop, exp, exp_new_val) in [
            (k1, '$.[*]', 0, 1, [0, 1, 2, 2],
             '[[],["a"],["a","b"],["a","b"]]'),
            (k2, '$.[*]', 1, 1, [0, 0, 1, 1], '[[],[],["b"],["b"]]'),
            (k3, '$.[*]', 1, 2, [0, 0, 1, 2, 2], '[[],[],[1],[1,2],[1,2]]')
        ]:
            assert exp == client.execute_command(
                'JSON.ARRTRIM', key, path, start, stop)
            assert exp_new_val == client.execute_command(
                'JSON.GET', key, '.').decode()

    def test_json_arrindex_command(self):
        # edge case: empty array
        client = self.server.get_new_client()
        assert -1 == client.execute_command(
            'JSON.ARRINDEX', wikipedia, '.children', '"tom"')

        # populate the array
        assert 5 == client.execute_command('JSON.ARRAPPEND', wikipedia, '.children',
                                           '"John"', '"Mary"', '"Tom"', '"Paul"', '"Peter"')

        for (val, idx) in [
            ('"John"', 0),
            ('"Tom"', 2),
            ('"Peter"', 4),
            ('"Peter2"', -1)
        ]:
            assert idx == client.execute_command(
                'JSON.ARRINDEX', wikipedia, '.children', val)

        for (val, start, stop, idx) in [
            ('"Tom"', 5, 0, -1),
            ('"Paul"', 0, 4, 3),
            ('"Paul"', 0, 0, 3)
        ]:
            assert idx == client.execute_command(
                'JSON.ARRINDEX', wikipedia, '.children', val, start, stop)

        assert b'OK' == client.execute_command(
            'JSON.SET', arr, '.', '[0, 1, 2, 3, 4]')
        assert 1 == client.execute_command(
            'JSON.ARRINDEX', arr, '.', '1')
        assert - \
            1 == client.execute_command(
                'JSON.ARRINDEX', arr, '.', '1', '2')

        # return error if document key does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINDEX', foo, '.children', 0, 5)
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.ARRINDEX', wikipedia, '.foo', 0, 3)
        assert self.error_class.is_nonexistent_error(str(e.value))

        # error condition: the element is not an array
        for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.ARRINDEX', wikipedia, path, '1')
            assert self.error_class.is_wrongtype_error(str(e.value))

        # test large index: larger than int32
        client.execute_command(
            'JSON.ARRINDEX', wikipedia, ".phoneNumbers", '1', 3000000000, 0)

    def test_json_arrindex_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[[], [0], [0,1], [0,1,2]]')
        assert b'OK' == client.execute_command('JSON.SET', k3, '.',
                                               '[[], [0,true], [0,1,false], [0,1,2,null,true]]')

        for (key, path, val, exp) in [
            (k1, '$.[*]', '"a"',  [-1, 0, 0, 0]),
            (k1, '$.[*]', '"b"',  [-1, -1, 1, 1]),
            (k1, '$.[*]', '"c"',  [-1, -1, -1, 2]),
            (k2, '$.[*]', '1',    [-1, -1, 1, 1]),
            (k2, '$.[*]', '2',    [-1, -1, -1, 2]),
            (k2, '$.[*]', 'true', [-1, -1, -1, -1]),
            (k3, '$.[*]', 'true', [-1, 1, -1, 4]),
            (k3, '$.[*]', 'null', [-1, -1, -1, 3])
        ]:
            assert exp == client.execute_command(
                'JSON.ARRINDEX', key, path, val)

    def test_json_arrindex_should_not_limit_to_scalar_value(self):
        client = self.server.get_new_client()
        client.execute_command(
            'JSON.SET', k1, '.', '[5, 6, {"a":"b"}, [99,100]]')
        assert 2 == client.execute_command(
            'JSON.ARRINDEX', k1, '.', '{"a":"b"}', 0, 0)
        assert 3 == client.execute_command(
            'JSON.ARRINDEX', k1, '.', '[99,100]', 0, 0)
        assert 0 == client.execute_command(
            'JSON.ARRINDEX', k1, '.', '5', 0, 0)
        assert 1 == client.execute_command(
            'JSON.ARRINDEX', k1, '.', '6', 0, 0)

        # Wrong number of arguments
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.ARRINDEX', wikipedia, '.phoneNumbers', '1', 0, 3, 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_arrindex_complex_v2_path(self):
        client = self.server.get_new_client()

        json_string = '{"level0":{"level1_0":{"level2":[1,2,3, [25, [4,5,{"c":"d"}]]]},"level1_1":{"level2":[[{"a":[2,5]},true,null]]}}}'
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', json_string)
        for (path, val, exp) in [
            ("$..level0.level1_0..", b'[4,5,{"c":"d"}]',         [
             None, -1, 1, -1, None]),
            ("$..level0.level1_0..", b'[25,[4,5,{"c":"d"}]]',    [
             None, 3, -1, -1, None]),
            ("$..level0.level1_0..", b'{"c":"d"}',
             [None, -1, -1, 2, None]),
            ("$..level0.level1_1..", b'[{"a":[2,5]},true,null]', [
             None, 0, -1, None, -1]),
            ("$..level0.level1_1..", b'[null,true,{"a":[2,5]}]', [
             None, -1, -1, None, -1]),
            ("$..level0.level1_1..", b'[{"a":[2,5]},true]',      [
             None, -1, -1, None, -1]),
            ("$..level0.level1_0..", b'[4,{"c":"d"}]',           [
             None, -1, -1, -1, None])
        ]:
            assert exp == client.execute_command(
                'JSON.ARRINDEX', k1, path, val)

    def test_json_type_command(self):
        client = self.server.get_new_client()
        for (path, type) in [
            ('.', 'object'),
            ('.groups', 'object'),
            ('.phoneNumbers', 'array'),
            ('.children', 'array'),
            ('.isAlive', 'boolean'),
            ('.spouse', 'null'),
            ('.address.city', 'string'),
            ('.age', 'integer'),
            ('.weight', 'number')
        ]:
            assert type == client.execute_command(
                'JSON.TYPE', wikipedia, path).decode('utf-8')

        # return should be null if document key does not exist
        assert None == client.execute_command('JSON.TYPE', foo)

        # return should be null if path does not exist
        assert None == client.execute_command(
            'JSON.TYPE', wikipedia, '.foo')

        # Wrong number of arguments

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.TYPE', wikipedia, '.phoneNumbers', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_type_command_jsonpath(self):
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '{"a":1, "b":2.3, "c":"foo", "d":true, "e":null, "f":{}, "g":[]}')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '[1, 2.3, "foo", true, null, {}, []]')

        for (key, path, exp) in [
            (k1, '$.*',  [b"integer", b"number", b"string",
             b"boolean", b"null", b"object", b"array"]),
            (k2, '$[*]', [b"integer", b"number", b"string",
             b"boolean", b"null", b"object", b"array"])
        ]:
            assert exp == client.execute_command(
                'JSON.TYPE', key, path)

    def test_json_resp_command(self):
        client = self.server.get_new_client()
        for (path, res) in [
            ('.firstName', b'John'),
            ('.isAlive', b'true'),
            ('.age', 27),
            ('.spouse', None)
        ]:
            assert res == client.execute_command(
                'JSON.RESP', wikipedia, path)

        for (path, res) in [('.weight', '135.17')]:
            assert res == client.execute_command(
                'JSON.RESP', wikipedia, path).decode()

        arr = client.execute_command(
            'JSON.RESP', wikipedia, '.children')
        assert 1 == len(arr)
        assert arr == [b'[']

        arr = client.execute_command(
            'JSON.RESP', wikipedia, '.address')
        assert 5 == len(arr)

        assert arr[0:4] == [b'{', [b'street', b'21 2nd Street'], [
            b'city', b'New York'], [b'state', b'NY']]
        assert b'10021-3100' == arr[4][1]

        arr = client.execute_command(
            'JSON.RESP', wikipedia, '.phoneNumbers')
        assert 3 == len(arr)
        assert b'[' == arr[0]

        assert 3 == len(arr[1])
        assert arr[1] == [b'{', [b'type', b'home'],
                          [b'number', b'212 555-1234']]
        assert 3 == len(arr[2])
        assert arr[2] == [b'{', [b'type', b'office'],
                          [b'number', b'646 555-4567']]

        # return should be null if document key does not exist
        assert None == client.execute_command('JSON.RESP', foo)

        # error condition: path does not exist
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.RESP', wikipedia, '.foo')
        assert self.error_class.is_nonexistent_error(str(e.value))

        # Wrong number of arguments

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.RESP', wikipedia, '.phoneNumbers', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

    def test_json_resp_command_jsonpath(self):
        client = self.server.get_new_client()

        for (path, res) in [
            ('$.firstName',  [b'John']),
            ('$.isAlive',    [b'true']),
            ('$.age',        [27]),
            ('$.spouse',     [None]),
            ('$.foo',        [])
        ]:
            assert res == client.execute_command(
                'JSON.RESP', wikipedia, path)

        for (path, res) in [('$.weight', '135.17')]:
            assert res == client.execute_command(
                'JSON.RESP', wikipedia, path)[0].decode()

        arr = client.execute_command(
            'JSON.RESP', wikipedia, '$.children')
        assert 1 == len(arr)
        assert arr == [[b'[']]

        arr = client.execute_command(
            'JSON.RESP', wikipedia, '$.address.*')
        assert 4 == len(arr)
        assert arr == [b'21 2nd Street', b'New York', b'NY', b'10021-3100']

        arr = client.execute_command(
            'JSON.RESP', wikipedia, '$.phoneNumbers.*')
        assert 2 == len(arr)
        assert arr[0] == [b'{', [b'type', b'home'],
                          [b'number', b'212 555-1234']]
        assert arr[1] == [b'{', [b'type', b'office'],
                          [b'number', b'646 555-4567']]

    def test_json_debug_memory(self):
        # non-existent key
        client = self.server.get_new_client()

        assert None == client.execute_command(
            'JSON.DEBUG MEMORY', nonexistentkey)

        # non-existent path
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.DEBUG MEMORY', wikipedia, nonexistentpath)
        assert self.error_class.is_nonexistent_error(str(e.value))

        # syntax error: key not provided
        with pytest.raises(ResponseError) as e:
            client.execute_command('JSON.DEBUG MEMORY')
        assert str(e.value).startswith('wrong number of arguments')

        # syntax error: wrong subcommand
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.DEBUG MEMORY123', wikipedia)
        assert self.error_class.is_syntax_error(str(e.value))

        with pytest.raises(ResponseError) as e:
            client.execute_command('JSON.DEBUG M', wikipedia)
        assert self.error_class.is_syntax_error(str(e.value))

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.DEBUG MEMORY', wikipedia, '.', 'extra')
        assert str(e.value).find('wrong number of arguments') >= 0

        # Verify the document size calculated by the "per document memory tracking" machinery matches the size
        # calculated by the method of walking the JSON tree.
        metadate_val = client.execute_command('JSON.DEBUG','MEMORY',wikipedia)
        exp_val = client.execute_command('JSON.DEBUG','MEMORY',wikipedia,'.')
        assert exp_val == metadate_val

    def test_json_duplicate_keys(self):
        client = self.server.get_new_client()
        '''Test handling of object with duplicate keys'''
        client.execute_command(
            'JSON.SET', k1, '.', '{"a":0, "a":1}')
        assert b'{"a":1}' == client.execute_command(
            'JSON.GET', k1, '.')
        assert [b"a"] == client.execute_command(
            'JSON.OBJKEYS', k1)
        assert b'1' == client.execute_command(
            'JSON.GET', k1, 'a')
        assert 1 == client.execute_command('JSON.OBJLEN', k1)
        client.execute_command('JSON.SET', k1, 'a', '2')
        assert b'{"a":2}' == client.execute_command(
            'JSON.GET', k1, '.')
        client.execute_command('JSON.NUMINCRBY', k1, 'a', '2')
        assert b'{"a":4}' == client.execute_command(
            'JSON.GET', k1, '.')
        client.execute_command('JSON.NUMMULTBY', k1, 'a', '2')
        assert b'{"a":8}' == client.execute_command(
            'JSON.GET', k1, '.')
        client.execute_command('JSON.DEL', k1, 'a')
        assert b'{}' == client.execute_command(
            'JSON.GET', k1, '.')

    def test_json_set_command_max_depth(self):
        client = self.server.get_new_client()

        def json_with_depth(depth):
            return '{"a":'*depth + '{}' + '}'*depth

        depth_limit = 128
        client.execute_command(
            'config set json.max-path-limit ' + str(depth_limit))

        # json not too deep: ok
        assert b'OK' == client.execute_command(
            'JSON.SET', k, '.', json_with_depth(127))

        # error condition: json is too deep
        with pytest.raises(ResponseError) as e:
            json_deep = json_with_depth(200000)
            client.execute_command(
                'JSON.SET', k, '.', json_deep)
        assert self.error_class.is_limit_exceeded_error(str(e.value))

    def test_json_set_command_max_size(self):
        client = self.server.get_new_client()

        def json_with_size(size):
            return '"' + 'a'*(size-2) + '"'

        MB = 2**20
        assert b'OK' == client.execute_command(
            'JSON.SET', k, '.', json_with_size(33*MB))

        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', k, '.', json_with_size(64*MB))
        assert self.error_class.is_limit_exceeded_error(str(e.value))

    def test_json_mset_command_supports_all_datatypes(self):
        client = self.server.get_new_client()
        for (path, value) in [('.address.city', '"Boston"'),            # string
                              # number
                              ('.age', '30'),
                              ('.foo', '[1,2,3]'),                      # array
                              # array element access
                              ('.phoneNumbers[0].number', '"1234567"'),
                              # object
                              ('.foo', '{"a":"b"}'),
                              ('.lastName', 'null'),                    # null
                              ('.isAlive', 'false')]:                   # boolean
            assert b'OK' == client.execute_command(
                'JSON.MSET', wikipedia, path, value)
            assert value.encode() == client.execute_command(
                'JSON.GET', wikipedia, path)

    def test_json_mset_command_root_document(self):
        client = self.server.get_new_client()
        # path to the root document is represented as '.'
        assert b'OK' == client.execute_command(
            "JSON.MSET",
            k1, ".", '"Boston"',
            k2, ".", '123',
            k3, ".", '["Seattle","Boston"]',
            k4, ".", '[1,2,3]',
            k5, ".", '{"a":"b"}',
            k6, ".", '{}',
            k7, ".", 'null',
            k8, ".", 'false')
        for (key, value) in [(k1, '"Boston"'),                # string
                             (k2, '123'),                     # number
                             (k3, '["Seattle","Boston"]'),    # array
                             (k4, '[1,2,3]'),                 # array
                             (k5, '{"a":"b"}'),               # object
                             (k6, '{}'),                      # empty object
                             (k7, 'null'),                    # null
                             (k8, 'false')]:                  # boolean
            assert value.encode() == client.execute_command('JSON.GET', key)

    def test_json_mset_command_with_error_conditions(self):
        client = self.server.get_new_client()
        # A new Valkey key's path must be root
        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                'JSON.MSET', foo, '.bar', '"bar"')
        assert self.error_class.is_syntax_error(str(e.value))

    def test_json_mset_command_mixed_path_document(self):
        client = self.server.get_new_client()
        # path to the root document is represented as '.'
        assert b'OK' == client.execute_command(
            "JSON.MSET",
            k1, ".", '"Boston"',
            wikipedia, '.address.city', '"Boston"',
            k2, ".", '123',
            wikipedia, '.age', '30',
            k3, ".", '["Seattle","Boston"]',
            k4, ".", '[1,2,3]',
            wikipedia, '.phoneNumbers[0].number', '"1234567"',
            k5, ".", '{"a":"b"}',
            wikipedia, '.foo', '{"a":"b"}',
            k6, ".", '{}',
            wikipedia, '.lastName', 'null',
            k7, ".", 'null',
            wikipedia, '.isAlive', 'false',
            k8, ".", 'false')
        for (key, path, value) in [(k1, ".", '"Boston"'),                # string
                                   (k2, ".", '123'),                     # number
                                   (k3, ".", '["Seattle","Boston"]'),    # array
                                   (k4, ".", '[1,2,3]'),                 # array
                                   (k5, ".", '{"a":"b"}'),               # object
                                   (k6, ".", '{}'),                      # empty object
                                   (k7, ".", 'null'),                    # null
                                   (k8, ".", 'false'),                  # boolean
                                   (wikipedia, '.address.city', '"Boston"'),
                                   (wikipedia, '.age', '30'),
                                   (wikipedia, '.phoneNumbers[0].number', '"1234567"'),
                                   (wikipedia, '.foo', '{"a":"b"}'),
                                   (wikipedia, '.lastName', 'null'),
                                   (wikipedia, '.isAlive', 'false')]:
            assert value.encode() == client.execute_command(
                'JSON.GET', key, path)

    def test_json_mset_command_atomicity(self):
        client = self.server.get_new_client()
        # path to the root document is represented as '.'
        assert b'OK' == client.execute_command(
            "JSON.MSET",
            k1, ".", '"Boston"',
            wikipedia, '.address.city', '"Boston"',
            k2, ".", '123',
            wikipedia, '.age', '30',
            k3, ".", '["Seattle","Boston"]',
            k4, ".", '[1,2,3]',
            wikipedia, '.phoneNumbers[0].number', '"1234567"',
            k5, ".", '{"a":"b"}',
            wikipedia, '.foo', '{"a":"b"}',
            k6, ".", '{}',
            wikipedia, '.lastName', 'null',
            k7, ".", 'null',
            wikipedia, '.isAlive', 'false',
            k8, ".", 'false')

        with pytest.raises(ResponseError) as e:
            assert None == client.execute_command(
                "JSON.MSET",
                k1, ".", '"Seattle"',
                wikipedia, '.address.city', '"Seattle"',
                k2, ".", '456',
                wikipedia, '.age', '40',
                k3, ".", '["Seattle","Chicago"]',
                k4, ".", '[4,5,6]',
                wikipedia, '.phoneNumbers[0].number', '"56789"',
                k5, ".", '{"c":"d"}',
                wikipedia, '.foo', '{"c":"d"}',
                k6, ".", '{}',
                wikipedia, '.lastName', 'null',
                k7, ".", 'null',
                foo, '.bar', '"bar"',
                k8, ".", 'true')
        assert self.error_class.is_syntax_error(str(e.value))

        for (key, path, value) in [(k1, ".", '"Boston"'),                # string
                                   (k2, ".", '123'),                     # number
                                   (k3, ".", '["Seattle","Boston"]'),    # array
                                   (k4, ".", '[1,2,3]'),                 # array
                                   (k5, ".", '{"a":"b"}'),               # object
                                   (k6, ".", '{}'),                      # empty object
                                   (k7, ".", 'null'),                    # null
                                   (k8, ".", 'false'),                  # boolean
                                   (wikipedia, '.address.city', '"Boston"'),
                                   (wikipedia, '.age', '30'),
                                   (wikipedia, '.phoneNumbers[0].number', '"1234567"'),
                                   (wikipedia, '.foo', '{"a":"b"}'),
                                   (wikipedia, '.lastName', 'null'),
                                   (wikipedia, '.isAlive', 'false')]:
            assert value.encode() == client.execute_command(
                'JSON.GET', key, path)

    def test_multi_exec(self):
        client = self.server.get_new_client()
        client.execute_command('MULTI')
        client.execute_command(
            'JSON.DEL', wikipedia, '.address.street')
        client.execute_command(
            'JSON.DEL', wikipedia, '.address.zipcode')
        client.execute_command(
            'JSON.SET', wikipedia, '.address.region', '"US East"')
        client.execute_command('EXEC')
        v = client.execute_command(
            'JSON.GET', wikipedia, '.address').decode()
        assert v == '{"city":"New York","state":"NY","region":"US East"}' or \
            v == '{"state":"NY","city":"New York","region":"US East"}'

        client.execute_command('MULTI')
        client.execute_command(
            'JSON.ARRPOP', wikipedia, '.phoneNumbers')
        client.execute_command(
            'JSON.ARRPOP', wikipedia, '.phoneNumbers')
        client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.phoneNumbers', '123')
        client.execute_command(
            'JSON.ARRAPPEND', wikipedia, '.phoneNumbers', '456')
        client.execute_command('EXEC')
        v = client.execute_command(
            'JSON.GET', wikipedia, '.phoneNumbers').decode()
        assert v == '[123,456]'

    def test_escaped_member_names(self):
        '''
        Test accessing member names that contain escaped characters.
        '''
        client = self.server.get_new_client()

        assert b'OK' == client.execute_command('JSON.SET', k1, '.',
                                               '{"a\\\\a":1, "b\\tb":2, "c\\nc":3, "d\\rd":4, "e\\be":5, "f\\"f":6, "":7, "\'":8}')
        assert b'OK' == client.execute_command('JSON.SET', k2, '.',
                                               '{"key\\u0000":"value\\u0000", "key\\u001F":"value\\u001F"}')
        for (key, path, exp) in [
            (k1, '$["a\\\\a"]', '[1]'),
            (k1, "$['a\\a']",   '[1]'),
            (k1, '$["b\\tb"]',  '[2]'),
            (k1, "$['b\\tb']",  '[2]'),
            (k1, '$["c\\nc"]',  '[3]'),
            (k1, "$['c\\nc']",  '[3]'),
            (k1, '$["d\\rd"]',  '[4]'),
            (k1, "$['d\\rd']",  '[4]'),
            (k1, '$["e\\be"]',  '[5]'),
            (k1, "$['e\\be']",  '[5]'),
            (k1, '$["f\\"f"]',  '[6]'),
            (k1, "$['f\"f']",   '[6]'),
            (k1, '$[""]',       '[7]'),
            (k1, "$['']",       '[7]'),
            (k1, '$["\'"]',     '[8]'),
            (k1, "$['\\\'']",   '[8]'),
            (k2, '$["key\\u0000"]', '["value\\u0000"]'),
            (k2, '$["key\\u001F"]', '["value\\u001F"]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path).decode()

    def test_serializing_escaped_quotes_in_member_name(self):
        client = self.server.get_new_client()
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', '{"\\"a":1, "\\"b":2}')
        for (path, space, exp) in [
            ('.', None, '{"\\"a":1,"\\"b":2}'),
            ('.', ' ',  '{"\\"a": 1,"\\"b": 2}')
        ]:
            if space is None:
                assert exp == client.execute_command(
                    'JSON.GET', k1, path).decode()
            else:
                assert exp == client.execute_command(
                    'JSON.GET', k1, 'space', space, path).decode()

    def test_json_numincrby_jsonpath_and_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
        client.execute_command(
            'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')

        # JSONPath: return an array of values.
        # If a value is not a number, its corresponding returned element is JSON null.
        # NOTE: The expected value has accounted for the outcome of previous commands on the same key.
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMINCRBY', k1, '$.a.*', '1',   '[]'),
            ('JSON.GET',       k1, '$.a.*', None,  '[]'),
            ('JSON.NUMINCRBY', k1, '$.b.*', '1',   '[2]'),
            ('JSON.GET',       k1, '$.b.*', None,  '[2]'),
            ('JSON.NUMINCRBY', k1, '$.b[*]', '1',  '[3]'),
            ('JSON.GET',       k1, '$.b[*]', None, '[3]'),
            ('JSON.NUMINCRBY', k1, '$.d.*', '1',   '[2,3,4]'),
            ('JSON.GET',       k1, '$.d.*', None,  '[2,3,4]'),
            ('JSON.NUMINCRBY', k1, '$.d[*]', '1',  '[3,4,5]'),
            ('JSON.GET',       k1, '$.d[*]', None, '[3,4,5]'),
            ('JSON.NUMINCRBY', k2, '$.a.*', '1',   '[]'),
            ('JSON.GET',       k2, '$.a.*', None,  '[]'),
            ('JSON.NUMINCRBY', k2, '$.b.*', '1',   '[2]'),
            ('JSON.GET',       k2, '$.b.*', None,  '[2]'),
            ('JSON.NUMINCRBY', k2, '$.d.*', '1',   '[2,3,4]'),
            ('JSON.GET',       k2, '$.d.*', None,  '[2,3,4]'),
            ('JSON.NUMINCRBY', k3, '$.a.*', '1',   '[null]'),
            ('JSON.GET',       k3, '$.a.*', None,  '["a"]'),
            ('JSON.NUMINCRBY', k3, '$.b.*', '1',   '[null,2]'),
            ('JSON.GET',       k3, '$.b.*', None,  '["a",2]'),
            ('JSON.NUMINCRBY', k3, '$.c.*', '1',   '[null,null]'),
            ('JSON.GET',       k3, '$.c.*', None,  '["a","b"]'),
            ('JSON.NUMINCRBY', k3, '$.d.*', '1',   '[2,null,4]'),
            ('JSON.GET',       k3, '$.d.*', None,  '[2,"b",4]')
        ]:
            if incr_num is not None:
                assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
            else:
                assert exp.encode() == client.execute_command(cmd, key, path)

    def test_json_numincrby_legacy_path_and_wildcard(self):
        client = self.server.get_new_client()
        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
        client.execute_command(
            'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')

        # Legacy path: return NONEXISTENT error if no value is selected
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMINCRBY', k1, '.a.*', '1', None),
            ('JSON.NUMINCRBY', k2, '.a.*', '1', None)
        ]:
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    cmd, key, path, incr_num)
            assert self.error_class.is_nonexistent_error(str(e.value))
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    'JSON.GET', key, path)
            assert self.error_class.is_nonexistent_error(str(e.value))

        # Legacy path: return WRONGTYPE error if no number value is selected
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMINCRBY', k3, '.a.*', '1', None),
            ('JSON.NUMINCRBY', k3, '.c.*', '1', None)
        ]:
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    cmd, key, path, incr_num)
            assert self.error_class.is_wrongtype_error(str(e.value))

        # Legacy path: return a single value, which is the last updated value.
        # NOTE: The expected value has accounted for the outcome of previous commands on the same key.
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMINCRBY', k1, '.b.*', '1',   '2'),
            ('JSON.GET',       k1, '.b.*', None,  '2'),
            ('JSON.NUMINCRBY', k1, '.b[*]', '1',  '3'),
            ('JSON.GET',       k1, '.b[*]', None, '3'),
            ('JSON.NUMINCRBY', k1, '.d.*', '1',   '4'),
            ('JSON.GET',       k1, '.d.*', None,  '2'),
            ('JSON.NUMINCRBY', k1, '.d[*]', '1',  '5'),
            ('JSON.GET',       k1, '.d[*]', None, '3'),
            ('JSON.NUMINCRBY', k2, '.b.*', '1',   '2'),
            ('JSON.GET',       k2, '.b.*', None,  '2'),
            ('JSON.NUMINCRBY', k2, '.d.*', '1',   '4'),
            ('JSON.GET',       k2, '.d.*', None,  '2'),
            ('JSON.NUMINCRBY', k3, '.b.*', '1',   '2'),
            ('JSON.GET',       k3, '.b.*', None,  '"a"'),
            ('JSON.NUMINCRBY', k3, '.d.*', '1',   '4'),
            ('JSON.GET',       k3, '.d.*', None,  '2')
        ]:
            if incr_num is not None:
                assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
            else:
                assert exp.encode() == client.execute_command(cmd, key, path)

    def test_json_nummultby_jsonpath_and_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
        client.execute_command(
            'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')

        # JSONPath: return an array of values.
        # If a value is not a number, its corresponding returned element is JSON null.
        # NOTE: The expected value has accounted for the outcome of previous commands on the same key.
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMMULTBY', k1, '$.a.*', '2',   '[]'),
            ('JSON.GET',       k1, '$.a.*', None,  '[]'),
            ('JSON.NUMMULTBY', k1, '$.b.*', '2',   '[2]'),
            ('JSON.GET',       k1, '$.b.*', None,  '[2]'),
            ('JSON.NUMMULTBY', k1, '$.b[*]', '2',  '[4]'),
            ('JSON.GET',       k1, '$.b[*]', None, '[4]'),
            ('JSON.NUMMULTBY', k1, '$.d.*', '2',   '[2,4,6]'),
            ('JSON.GET',       k1, '$.d.*', None,  '[2,4,6]'),
            ('JSON.NUMMULTBY', k1, '$.d[*]', '2',  '[4,8,12]'),
            ('JSON.GET',       k1, '$.d[*]', None, '[4,8,12]'),
            ('JSON.NUMMULTBY', k2, '$.a.*', '2',   '[]'),
            ('JSON.GET',       k2, '$.a.*', None,  '[]'),
            ('JSON.NUMMULTBY', k2, '$.b.*', '2',   '[2]'),
            ('JSON.GET',       k2, '$.b.*', None,  '[2]'),
            ('JSON.NUMMULTBY', k2, '$.d.*', '2',   '[2,4,6]'),
            ('JSON.GET',       k2, '$.d.*', None,  '[2,4,6]'),
            ('JSON.NUMMULTBY', k3, '$.a.*', '2',   '[null]'),
            ('JSON.GET',       k3, '$.a.*', None,  '["a"]'),
            ('JSON.NUMMULTBY', k3, '$.b.*', '2',   '[null,2]'),
            ('JSON.GET',       k3, '$.b.*', None,  '["a",2]'),
            ('JSON.NUMMULTBY', k3, '$.c.*', '2',   '[null,null]'),
            ('JSON.GET',       k3, '$.c.*', None,  '["a","b"]'),
            ('JSON.NUMMULTBY', k3, '$.d.*', '2',   '[2,null,6]'),
            ('JSON.GET',       k3, '$.d.*', None,  '[2,"b",6]')
        ]:
            if incr_num is not None:
                assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
            else:
                assert exp.encode() == client.execute_command(cmd, key, path)

    def test_json_nummultby_legacy_path_and_wildcard(self):
        client = self.server.get_new_client()

        client.execute_command(
            'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
        client.execute_command(
            'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
        client.execute_command(
            'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')

        # Legacy path: return NONEXISTENT error if no value is selected
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMMULTBY', k1, '.a.*', '2', None),
            ('JSON.NUMMULTBY', k2, '.a.*', '2', None)
        ]:
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    cmd, key, path, incr_num)
            assert self.error_class.is_nonexistent_error(str(e.value))
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    'JSON.GET', key, path)
            assert self.error_class.is_nonexistent_error(str(e.value))

        # Legacy path: return WRONGTYPE error if no number value is selected
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMMULTBY', k3, '.a.*', '2', None),
            ('JSON.NUMMULTBY', k3, '.c.*', '2', None)
        ]:
            with pytest.raises(ResponseError) as e:
                assert exp == client.execute_command(
                    cmd, key, path, incr_num)

            assert self.error_class.is_wrongtype_error(str(e.value))

        # Legacy path: return a single value, which is the last updated value.
        # NOTE: The expected value has accounted for the outcome of previous commands on the same key.
        for (cmd, key, path, incr_num, exp) in [
            ('JSON.NUMMULTBY', k1, '.b.*', '2',   '2'),
            ('JSON.GET',       k1, '.b.*', None,  '2'),
            ('JSON.NUMMULTBY', k1, '.b[*]', '2',  '4'),
            ('JSON.GET',       k1, '.b[*]', None, '4'),
            ('JSON.NUMMULTBY', k1, '.d.*', '2',   '6'),
            ('JSON.GET',       k1, '.d.*', None,  '2'),
            ('JSON.NUMMULTBY', k1, '.d[*]', '2',  '12'),
            ('JSON.GET',       k1, '.d[*]', None, '4'),
            ('JSON.NUMMULTBY', k2, '.b.*', '2',   '2'),
            ('JSON.GET',       k2, '.b.*', None,  '2'),
            ('JSON.NUMMULTBY', k2, '.d.*', '2',   '6'),
            ('JSON.GET',       k2, '.d.*', None,  '2'),
            ('JSON.NUMMULTBY', k3, '.b.*', '2',   '2'),
            ('JSON.GET',       k3, '.b.*', None,  '"a"'),
            ('JSON.NUMMULTBY', k3, '.d.*', '2',   '6'),
            ('JSON.GET',       k3, '.d.*', None,  '2')
        ]:
            if incr_num is not None:
                assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
            else:
                assert exp.encode() == client.execute_command(cmd, key, path)

    def test_json_digest(self):
        client = self.server.get_new_client()
        orig_digest = client.execute_command('DEBUG', 'DIGEST')
        assert orig_digest != 0
        client.execute_command("flushall")
        new_digest = client.execute_command('DEBUG', 'DIGEST')
        assert int(new_digest) == 0

    def test_big_dup(self):
        client = self.server.get_new_client()
        # This test is to test memory leak

        for fle in glob.glob("data/*.json"):
            with open(fle, 'r') as file:
                self.data = file.read()
            logging.debug("File %s is size %d" % (fle, len(self.data)))
            b0 = client.info(JSON_INFO_METRICS_SECTION)[
                JSON_INFO_NAMES['total_memory_bytes']]
            try:
                client.execute_command(
                    "json.set x .", self.data)
            except:
                pass
            try:
                client.execute_command("json.del x .")
            except:
                pass
            b2 = client.info(JSON_INFO_METRICS_SECTION)[
                JSON_INFO_NAMES['total_memory_bytes']]
            assert b2 == b0

    def test_jsonpath_filter_expression(self):
        client = self.server.get_new_client()

        store = '''
        {
          "store": {
            "books": [
              {
                "category": "reference",
                "author": "Nigel Rees",
                "title": "Sayings of the Century",
                "price": 8.95
              },
              {
                "category": "fiction",
                "author": "Evelyn Waugh",
                "title": "Sword of Honour",
                "price": 12.99,
                "movies": [
                  {
                    "title": "Sword of Honour",
                    "realisator": {
                      "first_name": "Bill",
                      "last_name": "Anderson"
                    }
                  }
                ]
              },
              {
                "category": "fiction",
                "author": "Herman Melville",
                "title": "Moby Dick",
                "isbn": "0-553-21311-3",
                "price": 9
              },
              {
                "category": "fiction",
                "author": "J. R. R. Tolkien",
                "title": "The Lord of the Rings",
                "isbn": "0-395-19395-8",
                "price": 22.99
              }
            ],
            "bicycle": {
              "color": "red",
              "price": 19.95
            }
          }
        }
        '''
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', store)
        assert b'OK' == client.execute_command(
            'JSON.SET', k2, '.', '[1,2,3,4,5]')
        assert b'OK' == client.execute_command(
            'JSON.SET', k3, '.', '[true,false,true,false,null,1,2,3,4]')
        assert b'OK' == client.execute_command('JSON.SET', k4, '.',
                                               '{"books": [{"price":5,"sold":true,"in-stock":true,"title":"foo"}, {"price":15,"sold":false,"title":"abc"}]}')
        assert b'OK' == client.execute_command(
            'JSON.SET', k5, '.', '[1,2,3,4,5,6,7,8,9]')

        for (key, path, exp) in [
            (k1, '$.store.books[?(@.isbn)].price',
             b'[9,22.99]'),
            (k1, '$.store.books[?( @.isbn )].price',
             b'[9,22.99]'),
            (k1, '$.store.books[?(@["isbn"])]["price"]',
             b'[9,22.99]'),
            (k1, '$.store.books[?(@[ "isbn" ])][ "price" ]',
             b'[9,22.99]'),
            (k1, '$.store.books[?(@[\'isbn\'])][\'price\']',
             b'[9,22.99]'),
            (k1,
             '$.store.books[?(@.category == "reference")].price',      b'[8.95]'),
            (k1, '$.store.books[?(@.["category"] == "fiction")].price',
             b'[12.99,9,22.99]'),
            (k1, '$.store.books[?(@.price<1.0E1)].price',
             b'[8.95,9]'),
            (k1, '$.store.books[?(@["price"]<1.0E1)]["price"]',
             b'[8.95,9]'),
            (k1, '$.store.books[?(@.["price"]<1.0E1)]["price"]',
             b'[8.95,9]'),
            (k1, '$.store.books[?(@[\'price\']<1.0E1)][\'price\']',
             b'[8.95,9]'),
            (k1,
             '$.store.books[?(@.price>-1.23e1&&@.price<1.0E1)].price', b'[8.95,9]'),
            (k1, '$.store.books[?(@["price"]>-1.23e1&&@["price"]<1.0E1)]["price"]',
             b'[8.95,9]'),
            (k1, '$.store.books[?(@.["price"]>-1.23e1&&@.["price"]<1.0E1)].["price"]',    b'[8.95,9]'),
            (k1, '$.store.books[?(@["price"] > -1.23e1 && @["price"] < 1.0E1)]["price"]', b'[8.95,9]'),
            (k1, '$.store.books[?(@.price==22.99)].title',
             b'["The Lord of the Rings"]'),
            (k1, '$.store.books[?(@["price"]==22.99)].title',
             b'["The Lord of the Rings"]'),
            (k1,
             '$.store.books[?(@.price<10.0&&@.isbn)].price',           b'[9]'),
            (k1, '$.store.books[?(@.price<9||@.price>20)].price',
             b'[8.95,22.99]'),
            # precedence test
            (k1, '$.store.books[?(@.price<9||@.price>10&&@.isbn)].price',
             b'[8.95,22.99]'),
            # precedence test
            (k1,
             '$.store.books[?((@.price<9||@.price>10)&&@.isbn)].price', b'[22.99]'),
            # precedence test
            (k1,
             '$.store.books[?((@.price < 9 || @.price>10) && @.isbn)].price', b'[22.99]'),
            # precedence test
            (k1, '$.store.books[?((@["price"]<9||@["price"]>10)&&@["isbn"])]["price"]', b'[22.99]'),
            # precedence test
            (k1, '$.store.books[?((@["price"] < 9 || @["price"] > 10) && @["isbn"])]["price"]', b'[22.99]'),
            (k2, '$.*.[?(@>2)]',
             b'[3,4,5]'),
            (k2, '$.*.[?(@ > 2)]',
             b'[3,4,5]'),
            (k2, '$.*[?(@>2)]',
             b'[3,4,5]'),
            (k2, '$[*][?(@>2)]',
             b'[3,4,5]'),
            (k2, '$[ * ][?( @ > 2 )]',
             b'[3,4,5]'),
            (k2, '$.*[?(@ == 3)]',
             b'[3]'),
            (k2, '$.*[?(@ != 3)]',
             b'[1,2,4,5]'),
            (k3, '$.*.[?(@==true)]',
             b'[true,true]'),
            (k3, '$[*][?(@==true)]',
             b'[true,true]'),
            (k3, '$[*][?(@ == true)]',
             b'[true,true]'),
            (k3, '$.*.[?(@>1)]',
             b'[2,3,4]'),
            (k3, '$.*.[?( @ > 1 ) ]',
             b'[2,3,4]'),
            (k3, '$[*][?(@>1)]',
             b'[2,3,4]'),
            (k3, '$[ * ][?( @ > 1 ) ]',
             b'[2,3,4]'),
            (k4, '$.books[?(@.price>1&&@.price<20&&@.in-stock)]',
             b'[{"price":5,"sold":true,"in-stock":true,"title":"foo"}]'),
            (k4, '$.books[?(@[\'price\']>1&&@.price<20&&@["in-stock"])]',
             b'[{"price":5,"sold":true,"in-stock":true,"title":"foo"}]'),
            (k4, '$.books[?((@.price>1&&@.price<20)&&(@.sold==false))]',
             b'[{"price":15,"sold":false,"title":"abc"}]'),
            (k4, '$["books"][?((@["price"]>1&&@["price"]<20)&&(@["sold"]==false))]',
             b'[{"price":15,"sold":false,"title":"abc"}]'),
            (k5, '$.*[?(@ > 7 || @ < 3)]',
             b'[8,9,1,2]'),   # order test
            (k5, '$.*[?(@ < 3 || @ > 7)]',
             b'[1,2,8,9]'),   # order test
            (k5, '$.*[?(@ > 3 && @ < 7)]',
             b'[4,5,6]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '$.store.books[?(@.price<10.0)].price', '10.01')
        for (key, path, exp) in [
            (k1, '$.store.books[?(@.price<10.0)]',            b'[]'),
            (k1, '$.store.books[?(@.price<=10.02)].price',
             b'[10.01,10.01]'),
            (k1, '$.store.books[?(@.price <= 10.02)].price',
             b'[10.01,10.01]'),
            (k1, '$.store.books[?(@.price==10.01)].title',
             b'["Sayings of the Century","Moby Dick"]'),
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        for (key, path, new_val, exp) in [
            (k4, '$.books[?((@.price>1&&@.price<20)&&(@.sold==false))].price',
             '13.13', b'[13.13]'),
            (k4, '$.books[?((@.price > 1 && @.price < 20) && (@.sold == false))].price',
             '13.13', b'[13.13]'),
            (k4, '$["books"][?((@["price"]>1&&@["price"]<20)&&(@["sold"]==false))]["price"]',
             '13.13', b'[13.13]'),
            (k4, '$["books"][?((@["price"] > 1 && @["price"] < 20) && (@["sold"] == false))]["price"]', '13.13', b'[13.13]')
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, path, new_val)
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        # test delete with filter expression
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', store)
        assert b'OK' == client.execute_command(
            'JSON.SET', k2, '.', store)
        for (key, path, exp) in [
            (k1, '$.store.books[?(@.["category"] == "fiction")].price', 3),
            (k2, '$.store.books[?((@["price"] < 9 || @["price"] > 10) && @["isbn"])]["price"]', 1)
        ]:
            assert exp == client.execute_command(
                'JSON.DEL', key, path)
            assert b'[]' == client.execute_command(
                'JSON.GET', key, path)

    def test_jsonpath_recursive_descent(self):
        client = self.server.get_new_client()

        for (key, val) in [
            (k1, '{"a":{"a":1}}'),
            (k2, '{"a":{"a":{"a":{"a":1}}}}'),
            (k3, '{"x": {}, "y": {"a":"a"}, "z": {"a":"", "b":"b"}}'),
            (k4, '{"a":{"b":{"z":{"y":1}}, "c":{"z":{"y":2}}, "z":{"y":3}}}'),
            (k5, '{"a":1, "b": {"e":[0,1,2]}, "c":{"e":[10,11,12]}}'),
            (k6, '{"a":[1], "b": {"a": [2,3]}, "c": {"a": [4,5,6]}}')
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '.', val)

        for (key, path, exp) in [
            (k1, '$..a',          b'[{"a":1},1]'),
            (k2, '$..a',
             b'[{"a":{"a":{"a":1}}},{"a":{"a":1}},{"a":1},1]'),
            (k2, '$..a..a',       b'[{"a":{"a":1}},{"a":1},1]'),
            (k2, '$..a..a..a',    b'[{"a":1},1]'),
            (k2, '$..a..a..a..a', b'[1]'),
            (k2, '$..a..a..a..a..a', b'[]'),
            (k3, '$..a',          b'["a",""]'),
            (k4, '$.a..z.y',      b'[3,1,2]'),
            (k4, '$.a..z.*',      b'[3,1,2]'),
            (k4, '$.a.*..y',      b'[1,2,3]'),
            (k5, '$..e.[*]',      b'[0,1,2,10,11,12]'),
            (k5, '$..e[1]',       b'[1,11]'),
            (k5, '$..e.[1]',      b'[1,11]'),
            (k5, '$..e.[1]',      b'[1,11]'),
            (k5, '$..e[0:2]',     b'[0,1,10,11]'),
            (k5, '$..["e"][1]',   b'[1,11]'),
            (k5, '$..["e"][1]',   b'[1,11]'),
            (k5, '$..["e"][0:2]', b'[0,1,10,11]'),
            (k5, '$..[ "e" ][ 0 : 2 ]', b'[0,1,10,11]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        # recursive ARRAPPEND
        assert [2, 3, 4] == client.execute_command(
            'JSON.ARRAPPEND', k6, '$..a', 0)
        assert b'{"a":[1,0],"b":{"a":[2,3,0]},"c":{"a":[4,5,6,0]}}' == client.execute_command(
            'JSON.GET', k6)

        # recursive delete
        assert 2 == client.execute_command(
            'JSON.DEL', k3, '$..a')
        assert b'{"x":{},"y":{},"z":{"b":"b"}}' == client.execute_command(
            'JSON.GET', k3)

        # This is the only case that diverges from ReJSON v2. We deleted 4 elements while they deleted 1.
        # The divergence comes from the order of deletion.
        # Note that "JSON.GET k2 $..a" returns 4 elements.
        assert 4 == client.execute_command(
            'JSON.DEL', k2, '$..a')
        assert b'{}' == client.execute_command('JSON.GET', k2)

    def test_jsonpath_recursive_insert_update_delete(self):
        '''
        Test recursive insert, update and delete.
        '''
        client = self.server.get_new_client()
        data_store = '''
        {
          "store": {
            "books": [
              {
                "category": "reference",
                "author": "Nigel Rees",
                "title": "Sayings of the Century",
                "price": 8.95,
                "in-stock": true
              },
              {
                "category": "fiction",
                "author": "Evelyn Waugh",
                "title": "Sword of Honour",
                "price": 12.99,
                "in-stock": true,
                "movies": [
                  {
                    "title": "Sword of Honour - movie",
                    "realisator": {
                      "first_name": "Bill",
                      "last_name": "Anderson"
                    }
                  }
                ]
              },
              {
                "category": "fiction",
                "author": "Herman Melville",
                "title": "Moby Dick",
                "isbn": "0-553-21311-3",
                "price": 9,
                "in-stock": false
              },
              {
                "category": "fiction",
                "author": "J. R. R. Tolkien",
                "title": "The Lord of the Rings",
                "isbn": "0-115-03266-2",
                "price": 22.99,
                "in-stock": true
              },
              {
                "category": "reference",
                "author": "William Jr. Strunk",
                "title": "The Elements of Style",
                "price": 6.99,
                "in-stock": false
              },
              {
                "category": "fiction",
                "author": "Leo Tolstoy",
                "title": "Anna Karenina",
                "price": 22.99,
                "in-stock": true
              },
              {
                "category": "reference",
                "author": "Sarah Janssen",
                "title": "The World Almanac and Book of Facts 2021",
                "isbn": "0-925-23305-2",
                "price": 10.69,
                "in-stock": false
              },
              {
                "category": "reference",
                "author": "Kate L. Turabian",
                "title": "Manual for Writers of Research Papers",
                "isbn": "0-675-16695-1",
                "price": 8.59,
                "in-stock": true
              }
            ],
            "bicycle": {
              "color": "red",
              "price": 19.64,
              "in-stock": true
            }
          }
        }
        '''
        data_store2 = '''
        {
            "store": {
                "title": "foo",
                "bicycle": {
                    "title": "foo2",
                    "color": "red",
                    "price": 19.64,
                    "in-stock": true
                }
            }
        }
        '''
        for (key, val) in [
            (store, data_store),
            (store2, data_store2)
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '.', val)

        # recursive delete
        assert 2 == client.execute_command(
            'JSON.DEL', store2, '$..title')
        assert b'[]' == client.execute_command(
            'JSON.GET', store2, '$..title')

        assert b'{"store":{"bicycle":{"color":"red","price":19.64,"in-stock":true}}}' == client.execute_command(
            'JSON.GET', store2)

        # recursive insert, update and delete
        assert b'["Sayings of the Century","Sword of Honour","Sword of Honour - movie","Moby Dick","The Lord of the Rings","The Elements of Style","Anna Karenina","The World Almanac and Book of Facts 2021","Manual for Writers of Research Papers"]'\
               == client.execute_command('JSON.GET', store, '$..title')
        assert b'OK' == client.execute_command(
            'JSON.SET', store, '$..title', '"foo"')
        assert b'["foo","foo","foo","foo","foo","foo","foo","foo","foo"]'\
               == client.execute_command('JSON.GET', store, '$..title')
        for (key, path, exp) in [
            (store, '$.title',                                     b'[]'),
            (store, '$.store.title',                               b'[]'),
            (store, '$.store.bicycle.title',                       b'[]'),
            (store, '$.store.books[1].title',                      b'["foo"]'),
            (store, '$.store.books[1].movies[0].title',            b'["foo"]'),
            (store, '$.store.books[1].movies[0].realisator.title', b'[]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)
        assert 9 == client.execute_command(
            'JSON.DEL', store, '$..title')
        assert b'[]' == client.execute_command(
            'JSON.GET', store, '$..title')
        assert True == client.execute_command('save')

    def test_jsonpath_recursive_insert_update_delete2(self):
        '''
        Test recursive insert, update and delete.
        '''
        client = self.server.get_new_client()
        data_input = '''
        {
            "input": {
                "a": 1, 
                "b": {
                    "e": [0,1,2]
                }, 
                "c": {
                    "e": [10,11,12]
                }
            }
        }
        '''
        data_input2 = '''
        {
            "input": {
                "a": 1, 
                "b": {
                    "e": [0,1,2]
                }, 
                "c": {
                    "e": [10,11,12]
                }
            }
        }
        '''
        for (key, val) in [
            (input, data_input),
            (input2, data_input2)
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '.', val)

        # recursive delete
        assert 2 == client.execute_command(
            'JSON.DEL', input2, '$..e')
        assert b'[]' == client.execute_command(
            'JSON.GET', input2, '$..e')
        assert b'{"input":{"a":1,"b":{},"c":{}}}' == client.execute_command(
            'JSON.GET', input2)

        # recursive insert, update and delete
        assert b'[0,1,2,10,11,12]' == client.execute_command(
            'JSON.GET', input, '$..e[*]')
        assert b'OK' == client.execute_command(
            'JSON.SET', input, '$..e[*]', '4')
        assert b'[4,4,4,4,4,4]' == client.execute_command(
            'JSON.GET', input, '$..e[*]')
        assert b'[4,4,4]' == client.execute_command(
            'JSON.GET', input, '$.input.b.e[*]')
        for (key, path, exp) in [
            (input, '$.e',            b'[]'),
            (input, '$.input.e',      b'[]'),
            (input, '$.input.a.e',    b'[]'),
            (input, '$.input.b.e[*]', b'[4,4,4]'),
            (input, '$.input.c.e[*]', b'[4,4,4]'),
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)
        assert 2 == client.execute_command(
            'JSON.DEL', input, '$..e')
        assert 0 == client.execute_command(
            'JSON.DEL', input, '$..e')
        assert b'[]' == client.execute_command(
            'JSON.GET', input, '$..e')
        assert True == client.execute_command('save')

    def test_jsonpath_compatibility_invalidArrayIndex(self):
        client = self.server.get_new_client()

        # Array index is not integer
        for (key, path) in [
            (wikipedia, '.phoneNumbers[]'),
            (wikipedia, '.phoneNumbers[x]'),
            (wikipedia, '$.phoneNumbers[]'),
            (wikipedia, '$.phoneNumbers[x]')
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command('JSON.GET', key, path)
            assert self.error_class.is_syntax_error(str(e.value))

    def test_jsonpath_compatibility_unquotedMemberName(self):
        client = self.server.get_new_client()

        # Unquoted member name can contain any symbol except terminator characters
        json = '''{
            "%x22key%x22":1, "+2":2, "-3":3, "/4":4,
            ")5":5, "6)":6, "(7":7, "8(":8,
            "]9":9, "10]":10, "[11":11, "12[":12,
            ">13":13, "14>":14, "<15":15, "16<":16,
            "=17":17, "18=":18, "!19":19, "20!":20,
            "21.21":21
        }'''
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', json)

        test_cases = [
            (k1, '$.%x22key%x22',  b'[1]'),
            (k1, '$.+2',           b'[2]'),
            (k1, '$.-3',           b'[3]'),
            (k1, '$./4',           b'[4]'),
            # The following should return empty array because unquoted member name cannot contain terminator characters.
            (k1, '$.6)',           b'[]'),
            (k1, '$.8(',           b'[]'),
            (k1, '$.10]',          b'[]'),
            # Bracketed/Quoted member name should work
            (k1, '$["6)"]',        b'[6]'),
            (k1, '$["8("]',        b'[8]'),
            (k1, '$["10]"]',       b'[10]'),
            (k1, '$["12["]',       b'[12]'),
            (k1, '$["14>"]',       b'[14]'),
            (k1, '$["16<"]',       b'[16]'),
            (k1, '$["18="]',       b'[18]'),
            (k1, '$["20!"]',       b'[20]'),
            (k1, '$["21.21"]',     b'[21]'),
            (k1, '$.12[',          b'[]'),
            (k1, '$.14>',          b'[]'),
            (k1, '$.16<',          b'[]'),
            (k1, '$.18=',          b'[]'),
            (k1, '$.20!',          b'[]'),
            (k1, '$.21.21',        b'[]'),
        ]

        for (key, path, exp) in test_cases:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        # Unquoted object member cannot start with a character that is a member name terminator.
        for (key, path) in [
            (k1, '$.)5'),
            (k1, '$.]7'),
            (k1, '$.]9'),
            (k1, '$.[11'),
            (k1, '$.>13'),
            (k1, '$.<15'),
            (k1, '$.=17'),
            (k1, '$.!19')
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command('JSON.GET', key, path)
            assert self.error_class.is_syntax_error(str(e.value))

    def test_write_commands_duplicate_values(self):
        '''
        Test all WRITE JSON commands for the situation that if the json path results in multiple values
        with duplicates, the write operation should apply to all unique values once.
        '''
        client = self.server.get_new_client()

        for (key, val) in [
            (k1,  '[0,1,2,3,4,5,6,7,8,9]'),
            (k2,  '[0,1,2,3,4,5,6,7,8,9]'),
            (k3,  '[0,1,2,3,4,5,6,7,8,9]'),
            (k4,  '["0","1","2","3","4","5","6","7","8","9"]'),
            (k5,  '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
            (k6,  '[true,true,true,true,true]'),
            (k7,  '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
            (k8,  '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
            (k9,  '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
            (k10, '[[0,1,2,3,4],[0,1,2,3,4],[0,1,2,3,4]]'),
            (k11, '[0,1,2,3,4,5,6,7,8,9]'),
            (k12, '[0,1,2,3,4,5,6,7,8,9]')
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '.', val)

        # NUMINCRBY
        assert b'[10]' == client.execute_command(
            'JSON.NUMINCRBY', k1, '$[0,0,0,0,0]', 10)
        assert b'[10,1,2,3,4,5,6,7,8,9]' == client.execute_command(
            'JSON.GET', k1)

        # NUMMULTBY
        assert b'[18]' == client.execute_command(
            'JSON.NUMMULTBY', k2, '$[9,9,9,9,9]', 2)
        assert b'[0,1,2,3,4,5,6,7,8,18]' == client.execute_command(
            'JSON.GET', k2)

        # CLEAR
        assert 1 == client.execute_command(
            'JSON.CLEAR', k5, '$[0,0,0,0,0]')
        assert b'[[],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
            'JSON.GET', k5)

        # TOGGLE
        assert [0] == client.execute_command(
            'JSON.TOGGLE', k6, '$[0,0,0,0]')
        assert b'[false,true,true,true,true]' == client.execute_command(
            'JSON.GET', k6)

        # STRAPPEND
        assert [4] == client.execute_command(
            'JSON.STRAPPEND', k4, '$[0,0,0,0,0]', '"foo"')
        assert b'["0foo","1","2","3","4","5","6","7","8","9"]' == client.execute_command(
            'JSON.GET', k4)

        # ARRAPPEND
        assert [3] == client.execute_command(
            'JSON.ARRAPPEND', k7, '$[0,0,0,0,0]', 8, 9)
        assert b'[[0,8,9],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
            'JSON.GET', k7)

        # ARRINSERT
        assert [2] == client.execute_command(
            'JSON.ARRINSERT', k8, '$[0,0,0,0,0]', 0, 9)
        assert b'[[9,0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
            'JSON.GET', k8)

        # ARRPOP
        assert [b"0"] == client.execute_command(
            'JSON.ARRPOP', k9, '$[0,0,0,0,0]')
        assert b'[[],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
            'JSON.GET', k9)

        # ARRTRIM
        assert [2] == client.execute_command(
            'JSON.ARRTRIM', k10, '$[0,0,0,0,0]', 0, 1)
        assert b'[[0,1],[0,1,2,3,4],[0,1,2,3,4]]' == client.execute_command(
            'JSON.GET', k10)

        # DEL
        assert 1 == client.execute_command(
            'JSON.DEL', k3, '$[0,0,0,0,0]')
        assert b'[1,2,3,4,5,6,7,8,9]' == client.execute_command(
            'JSON.GET', k3)

        # DEL: delete arbitrary elements with duplicates
        assert 5 == client.execute_command(
            'JSON.DEL', k12, '$[1,4,7,0,0,3,3]')
        assert b'[2,5,6,8,9]' == client.execute_command(
            'JSON.GET', k12)

        # SET
        assert b'OK' == client.execute_command(
            'JSON.SET', k11, '$[0,0,0,0,0]', 9)
        assert b'[9,1,2,3,4,5,6,7,8,9]' == client.execute_command(
            'JSON.GET', k11)

    def test_jsonpath_compatibility_filterOnObject_and_stringComparison(self):
        client = self.server.get_new_client()

        json = '''
            {  
                "key for key"     : "key inside here",
                "an object"       : {
                  "weight"  : 300,
                  "a value" : 300,
                  "my key"  : "key inside here"
                },
                "another object" : {
                  "weight"  : 400,
                  "a value" : 400,
                  "my key"  : "key inside there"
                },
                "objects": [
                    {
                        "weight"  : 100,
                        "a value" : 100,
                        "my key"  : "key inside here"
                    },
                    {
                        "weight"  : 200,
                        "a value" : 200,
                        "my key"  : "key inside there"
                    },
                    {
                        "weight"  : 300,
                        "a value" : 300,
                        "my key"  : "key inside here"
                    },
                    {
                        "weight"  : 400,
                        "a value" : 400,
                        "my key"  : "key inside there"
                    }
                ]
            }
        '''
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '.', json)

        for (key, path, exp) in [
            (k1, '$["an object"].[?(@.weight > 200)].["a value"]',
             b'[300]'),
            (k1, '$["an object"].[?(@.weight == 300)].["a value"]',
             b'[300]'),
            (k1, '$["an object"].[?(@.weight > 300)].["a value"]',
             b'[]'),
            (k1, '$["another object"].[?(@["a value"] > 200)].weight',
             b'[400]'),
            (k1, '$["another object"].[?(@.["a value"] > 200)].weight',
             b'[400]'),
            (k1, '$["another object"].[?(@.["my key"] == "key inside there")].weight', b'[400]'),
            (k1, '$["objects"].[?(@.weight > 200)].["a value"]',
             b'[300,400]'),
            (k1, '$["objects"].[?(@.["my key"] == "key inside there")].weight',
             b'[200,400]'),
            (k1, '$["objects"].[?(@.["my key"] != "key inside there")].weight',
             b'[100,300]'),
            (k1, '$["objects"].[?(@.["my key"] == "key inside here")].weight',
             b'[100,300]'),
            (k1, '$["objects"].[?(@.["my key"] != "key inside here")].weight',
             b'[200,400]'),
            (k1, '$["objects"].[?(@.["my key"] <= "key inside here")].weight',
             b'[100,300]'),
            (k1, '$["objects"].[?(@.["my key"] >= "key inside here")].weight',
             b'[100,200,300,400]'),
            (k1, '$["objects"].[?(@.["my key"] < "key inside herf")].weight',
             b'[100,300]'),
            (k1, '$["objects"].[?(@.["my key"] > "key insidd here")].weight',
             b'[100,200,300,400]'),
            (k1, '$["key for key"].[?(@.["a value"] > 200)]',
             b'[]')
        ]:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

    def test_jsonpath_compatibility_union_of_object_members(self):
        client = self.server.get_new_client()

        test_cases = [
            (wikipedia, '$.["firstName","lastName"]',
             b'["John","Smith"]'),
            (organism,
             '$.animals["2","2"].mammals..weight',                 b'[]'),
            (organism,  '$.animals["2","Junk"]',
             b'[]'),
            (organism,  '$.animals["Junk","Junk"]',
             b'[]'),
            # test unique values in recursive descent
            (organism,  '$..[?(@.weight>400)].name',
             b'["Redwood","Horse"]'),
            (organism,  '$..[?(@.weight>=60&&@.name=="Chimpanzee")].name',
             b'["Chimpanzee"]'),
            (wikipedia, '$.[ "firstName", "lastName" ]',
             b'["John","Smith"]'),
            (wikipedia, '$.address.[ "street", "city", "state", "zipcode" ]',
             b'["21 2nd Street","New York","NY","10021-3100"]'),
        ]

        assert b'OK' == client.execute_command(
            'JSON.SET', organism, '.', DATA_ORGANISM)

        for (key, path, exp) in test_cases:
            assert exp == self.client.execute_command('JSON.GET', key, path)

    def test_jsonpath_malformed_path(self):
        client = self.server.get_new_client()

        for (key, val) in [
            (k1, '{"store":{"book":[{"price":5,"sold":true,"in-stock":true,"title":"foo","author":"me","isbn":"978-3-16-148410-0"}]}}'),
            (k2, '[1,2,3,4,5,6,7,8,9,10]')
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '.', val)

        test_cases = [
            (k1, '$[0:2]$[0:1]$[0:2]$[0:2]$[0<2065>:2]$[0:2]', b'[]'),
            (k1, '$[0,1]',                                     b'[]'),
            (k2, '$.[\"a\"].[\"b\"].[\"c\"]',                  b'[]'),
            (k1, '$a.b.c.d',                                   b'[]'),
            (k2, '$a$b$c$d',                                   b'[]'),
        ]

        for (key, path, exp) in test_cases:
            assert exp == client.execute_command(
                'JSON.GET', key, path)

        for (key, path) in [
            (k1, '.[0:2].[0:1].[0:2].[0:2].[0<2065>:2].[0:2]'),
            (k1, '.[0:2]$[0:1]$[0:2]$[0:2]$[0<2065>:2]$[0:2]')
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command('JSON.GET', key, path)
            assert self.error_class.is_wrongtype_error(
                str(e.value)) or self.error_class.is_nonexistent_error(str(e.value))

        for (key, path) in [
            (k1, '&&$.store..price'),
            (k1, '!$.store..price'),
            (k1, '=$.store..price'),
            (k1, '=.store..price'),
            (k1, '||.store..price')
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command('JSON.GET', key, path)
            assert self.error_class.is_syntax_error(str(e.value))
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'JSON.SET', key, path, '0')
            assert self.error_class.is_syntax_error(str(e.value))

    def test_v2_path_limit_recursive_descent(self):
        client = self.server.get_new_client()

        depth_limit = 10
        client.execute_command(
            'config set json.max-path-limit ' + str(depth_limit))

        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '$', '{"a":0}')

        # repeatedly increasing path depth by 1 till the limit is reached.
        for _ in range(0, depth_limit-1):
            client.execute_command(
                'JSON.SET', k1, '$..a', '{"a":0}')
        # verify the path depth reaches the limit
        assert depth_limit == client.execute_command(
            'JSON.DEBUG', 'DEPTH', k1)

        # one more increase should exceed the path limit
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', k1, '$..a', '{"a":0}')
        assert str(e.value).startswith("LIMIT")

    def test_v2_path_limit_insert_member(self):
        client = self.server.get_new_client()

        depth_limit = 3
        client.config_set('json.max-path-limit', depth_limit)

        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '$', '{"a":{"a":{"a":0}}}')
        assert depth_limit == client.execute_command(
            'JSON.DEBUG DEPTH', k1)

        # insert a nesting object
        with pytest.raises(ResponseError) as e:
            client.execute_command(
                'JSON.SET', k1, '$.a.a.b', '{"b":0}')
        assert str(e.value).startswith("LIMIT")

        # increase limit by 1
        client.config_set(
            'json.max-path-limit', depth_limit + 1)
        assert b'OK' == client.execute_command(
            'JSON.SET', k1, '$.a.a.b', '{"b":0}')

    def test_debug_command_getkeys_api(self):
        client = self.server.get_new_client()

        for (subcmd, res) in [
            ('memory k1', [b'k1']),
            ('fields k1', [b'k1']),
            ('depth k1',  [b'k1']),
            ('memory k1', [b'k1'])
        ]:
            assert res == client.execute_command(
                'command getkeys json.debug ' + subcmd)

        for subcmd in [
            '',
            'memory',
            'mem',
            'memo'
            'fields'
            'depth'
            'help',
            'max-depth-key',
            'max-size-key'
        ]:
            with pytest.raises(ResponseError) as e:
                client.execute_command(
                    'command getkeys json.debug ' + subcmd)
            assert self.error_class.is_wrong_number_of_arguments_error(str(e.value)) or \
                str(e.value).lower().find("invalid command") >= 0 or \
                str(e.value).lower().find(
                    "the command has no key arguments") >= 0

    def test_debug_command_depth(self):
        client = self.server.get_new_client()
        for (key, val, depth) in [
            (k1, '1',                     0),
            (k2, '"a"',                   0),
            (k3, 'null',                  0),
            (k4, 'true',                  0),
            (k5, '{}',                    0),
            (k6, '{"a":0}',               1),
            (k7, '{"a":{"a":0}}',         2),
            (k7, '{"a":{"a":{"a":0}}}',   3),
            (k8, '[]',                    0),
            (k9, '[0]',                   1),
            (k10, '[[0]]',                2),
            (k11, '[[0],[[0]]]',          3),
        ]:
            assert b'OK' == client.execute_command(
                'JSON.SET', key, '$', val)
            assert depth == client.execute_command(
                'JSON.DEBUG DEPTH', key)

        # what if the key does not exist?
        assert None == client.execute_command(
            'JSON.DEBUG DEPTH', 'foobar')

    def test_insert_update_delete_mode(self):
        client = self.server.get_new_client()
        assert b'OK' == client.execute_command(
            'JSON.SET', organism, '.', DATA_ORGANISM)
        assert b'OK' == client.execute_command(
            'JSON.SET', organism2, '.', DATA_ORGANISM)

        # delete
        key = organism
        path = '$.animals[*].mammals[*].primates[*].apes[?(@.weight<400)].weight'
        assert b'[130,300]' == client.execute_command(
            'JSON.GET', key, path)
        assert 2 == client.execute_command(
            'JSON.DEL', key, path)
        assert b'[]' == client.execute_command(
            'JSON.GET', key, path)

        # insert

        assert b'OK' == client.execute_command(
            'JSON.SET', key, path, 25)
        assert b'[]' == client.execute_command(
            'JSON.GET', key, path)

        # update
        key = organism2
        assert b'[130,300]' == client.execute_command(
            'JSON.GET', key, path)
        assert b'OK' == client.execute_command(
            'JSON.SET', key, path, 25)
        assert b'[25,25]' == client.execute_command(
            'JSON.GET', key, path)
        assert 2 == client.execute_command(
            'JSON.DEL', key, path)
        assert b'[]' == client.execute_command(
            'JSON.GET', key, path)

    def test_json_arity_per_command(self):
        client = self.server.get_new_client()

        # These commands should only get the single key
        cmd_arity = [('SET', -4), ('GET', -2), ('MGET', -3), ('DEL', -2), ('FORGET', -2), ('NUMINCRBY', 4),
                     ('NUMMULTBY', 4), ('STRLEN', -2), ('STRAPPEND', -
                                                        3), ('TOGGLE', -2), ('OBJLEN', -2), ('OBJKEYS', -2),
                     ('ARRLEN', -2), ('ARRAPPEND', -4), ('ARRPOP', -
                                                         2), ('ARRINSERT', -5), ('ARRTRIM', 5), ('CLEAR', -2),
                     ('ARRINDEX', -4), ('TYPE', -2), ('RESP', -2), ('DEBUG', -2)]

        for cmd, arity in cmd_arity:
            assert arity == client.execute_command('COMMAND', 'INFO', f'JSON.{cmd}')[
                f'JSON.{cmd}']['arity']

        cmd_arity = [('MEMORY', -3), ('FIELDS', -3), ('DEPTH', 3), ('HELP', 2),
                     ('MAX-DEPTH-KEY', 2), ('MAX-SIZE-KEY',
                                            2), ('KEYTABLE-CHECK', 2), ('KEYTABLE-CORRUPT', 3),
                     ('KEYTABLE-DISTRIBUTION', 3), ('TEST-SHARED-API', -2)]
        subcmd_dict = {f'JSON.DEBUG|{cmd}': arity for cmd, arity in cmd_arity}

        output = client.execute_command(
            'COMMAND', 'INFO', f'JSON.DEBUG')[f'JSON.DEBUG']['subcommands']
        assert len(output) == len(subcmd_dict)

        for i in range(len(output)):
            assert subcmd_dict[output[i][0].decode('ascii')] == output[i][1]

    def test_doc_mem_size_with_numincrby(self):
        """
        Verify the correctness of json document size.
        """
        client = self.server.get_new_client()

        # Set JSON key
        key = k1
        json = "{\"a\":1955439684,\"b\":5398,\"c\":[],\"d\":{\"e\":0,\"f\":2.7233281135559082,\"g\":1732953600,\"h\":false,\"i\":true}}"
        cmd = f'JSON.SET {key} . {json}'
        assert b'OK' == client.execute_command(cmd)

        for _ in range(0, 10000):
            # Increment the value by a large double and then set it to 0, so that the value flips between 0 and double.
            # Since double is stored as string, each flip changes the memory size of the value.
            cmd = f'json.numincrby {key} .d.e 7.055081799084578998899889890890'
            client.execute_command(cmd)

            cmd = f'json.set {key} .d.e 0'
            assert b'OK' == client.execute_command(cmd)

            # This value is the meta data updated by the "per key memory tracking" machinery
            cmd = f'json.debug memory {key}'
            metadata_val = client.execute_command(cmd)
            assert metadata_val < 1000

            # This value the malloc'ed size calculated by walking through the tree
            cmd = f'json.debug memory {key} .'
            exp_val = client.execute_command(cmd)
            assert exp_val == metadata_val

    def test_verify_doc_mem_size(self):
        """
        Verify per key doc memory size is correct.
        """
        client = self.server.get_new_client()
        src_dir = os.getenv('SOURCE_DIR')
        data_dir = f"{src_dir}/tst/integration/data/"
        file_names = os.listdir(data_dir)
        for file_name in file_names:
            with open(f'{data_dir}/{file_name}', 'r') as f:
                logging.info(f'Loading {file_name}')
                data = f.read()
            base_name = os.path.splitext(file_name)[0]
            key_name = base_name
            logging.info(f"Setting key {key_name}")
            client.execute_command('json.set', key_name, '.', data)

        # Scan keyspace
        count = 0
        cursor = 0
        while True:
            result = client.execute_command("SCAN", cursor, "TYPE", "ReJSON-RL")
            for key in result[1]:
                logging.info(f"Verifying memory size of key {key}")
                # This value is the meta data updated by the "per key memory tracking" machinery
                metadata_val = client.execute_command("JSON.DEBUG", "MEMORY", key)
                # This value the malloc'ed size calculated by walking through the tree
                exp_val = client.execute_command("JSON.DEBUG", "MEMORY", key, ".")
                assert exp_val == metadata_val
                assert metadata_val < SIZE_64MB
                count += 1

            if result[0] == 0:
                break
            cursor = result[0]
        logging.info(f"Verified {count} json keys")

    def test_jsonpath_member_name_containing_slash(self):
        """
        Test case for https://github.com/valkey-io/valkey-json/issues/31
        """
        client = self.server.get_new_client()
        client.execute_command("json.set", "k1", ".", "{\"a\": 4.5, \"b/c\": 85}")

        # Test 1: json.get with member name containing slash
        exp = '[85]'
        assert exp.encode() == client.execute_command("json.get", "k1", "$.b/c")

        # Test 2: json.set with member name containing slash
        client.execute_command("json.set", "k1", "$.b/c", "0")
        exp = "{\"a\":4.5,\"b/c\":0}"
        assert exp.encode() == client.execute_command("json.get", "k1")

        # Test 3: Slash is already escaped
        client.execute_command("json.set", "k1", "$.b~1c", "2")
        exp = "{\"a\":4.5,\"b/c\":2}"
        assert exp.encode() == client.execute_command("json.get", "k1")

        # Test 4: json.numincrby with member name containing slash
        client.execute_command("json.numincrby", "k1", "$.b/c", "1")
        exp = "{\"a\":4.5,\"b/c\":3}"
        assert exp.encode() == client.execute_command("json.get", "k1")

        # Test 5: json.nummultby with member name containing slash
        client.execute_command("json.nummultby", "k1", "$.b/c", "2")
        exp = "{\"a\":4.5,\"b/c\":6}"
        assert exp.encode() == client.execute_command("json.get", "k1")

        # Test 6: json.get with member name containing slash
        exp = '[6]'
        assert exp.encode() == client.execute_command("json.get", "k1", "$.b/c")

        # Test 7: json.set with recursive search
        client.execute_command("json.set", "k1", "$..b/c", "7")
        exp = "{\"a\":4.5,\"b/c\":7}"
        assert exp.encode() == client.execute_command("json.get", "k1")

        # Test 8: json.numincrby with recursive search
        client.execute_command("json.numincrby", "k1", "$..b/c", "2")
        exp = "{\"a\":4.5,\"b/c\":9}"
        assert exp.encode() == client.execute_command("json.get", "k1")

    def test_json_shared_api(self):
        DATA = '''
        {
            "0": 0,
            "1": true,
            "2": "string",
            "3": [0.0, 1.1, 2.0],
            "4": {},
            "5": {"0": 1, "1": "string"}
        }
        '''
        client = self.server.get_new_client()
        client.execute_command("json.set", "k1", ".", DATA)
        print(client.execute_command("json.debug","help"))
        for path in ["$.0", "$.1", "$.2", "$.3", "$.4", "$.5", "$5.0", "$5.1"]:
            g = client.execute_command("json.get", "k1", path)
            s = client.execute_command("json.debug", "test-shared-api", "k1", path)
            print("For Path:", path, " json.get:", g, " shared-api:", s)
            assert (g == s)
