# License: Apache 2.0. See LICENSE file in root directory.
# Copyright(c) 2024 Intel Corporation. All Rights Reserved.

#test:donotrun:!dds

import pyrealdds as dds
from rspy import log, test

# Test dds.option mechanism and logic

with test.closure( 'read-only options' ):
    test.check( dds.option.from_json( ['1', 0, 'desc'] ).is_read_only() )
    test.check_false( dds.option.from_json( ['1', 0, 0, 'desc'] ).is_read_only() )  # default value -> not read-only
    test.check( dds.option.from_json(
        ['Asic Temperature',None,-40.0,125.0,0.0,0.0,'Current Asic Temperature (degree celsius)',['optional','read-only']] ).is_read_only() )
    test.check_equal_lists( dds.option.from_json(  # default==min==max, step==0 -> read-only!
        ['Stereo Baseline',50.20994567871094,50.20994567871094,50.20994567871094,0.0,50.20994567871094,'...',['read-only']] ).to_json(),
        ['Stereo Baseline',50.20994567871094,'...'] )
    test.check_equal(  # 1.1 => 1.10000002 as float, => 1.1000000000000001 as double
        dds.option.from_json( ['a', 1.1, 'desc'] ).get_value(),
        1.1 )

with test.closure( 'boolean' ):
    test.check_equal( dds.option.from_json( ['b', True, 'bool'] ).value_type(), 'boolean' )
    test.check_equal( dds.option.from_json( ['b', False, 'bool'] ).value_type(), 'boolean' )
    test.check_equal( dds.option.from_json( ['b', 0, 'bool', ['boolean']] ).value_type(), 'boolean' )
    test.check_equal( dds.option.from_json( ['b', 0, 'bool', ['boolean']] ).get_value(), False )
    test.check_equal( dds.option.from_json( ['b', 1, 'bool', ['boolean']] ).get_value(), True )
    test.check_equal_lists(
        dds.option.from_json( ['b', 1, 'bool', ['boolean']] ).to_json(),
        ['b', True, 'bool'] )
    test.check_throws( lambda:
        dds.option.from_json( ['b', 1., 'bool', ['boolean']] ),
        RuntimeError, 'not convertible to a boolean: 1.0' )
    test.check_throws( lambda:
        dds.option.from_json( ['b', 2, 'bool', ['boolean']] ),
        RuntimeError, 'not convertible to a boolean: 2' )
    test.check_equal( dds.option.from_json( ['b', False, True, 'bool'] ).value_type(), 'boolean' )
    test.check_equal( dds.option.from_json( ['b', False, None, 'bool', ['optional']] ).value_type(), 'boolean' )

with test.closure( 'enum' ):
    test.check_equal( dds.option.from_json( ['e1', 'a', ['a','b','c'], 'c', 'enum'] ).value_type(), 'enum' )
    test.check_equal( dds.option.from_json( ['e1', 'a', ['a','a','c'], 'c', 'enum'] ).value_type(), 'enum' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', 'd', [], 'c', 'enum'] ),
        RuntimeError, 'invalid enum value: "c"' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', 'a', None, 'c', 'enum'] ),
        RuntimeError, 'enum option requires a choices array' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', 'd', ['a','b','c'], 'c', 'enum'] ),
        RuntimeError, 'invalid enum value: "d"' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', 'a', ['a','b','c'], 'd', 'enum'] ),
        RuntimeError, 'invalid enum value: "d"' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', 'a', [None,'b','c'], 'd', 'enum'] ),
        RuntimeError, 'enum choices must be strings' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', 1, [1,2,3], 3, 'enum'] ),
        RuntimeError, 'non-string enum values' )
    test.check_throws( lambda:
        dds.option.from_json( ['e1', None, ['a','b','c'], 'c', 'enum'] ),
        RuntimeError, 'value is not optional' )
    test.check_equal( dds.option.from_json( ['e1', None, ['a','b','c'], 'c', 'enum', ['optional']] ).value_type(), 'enum' )
    test.check_equal_lists(
        dds.option.from_json( ['e1', None, ['a','b','c'], 'c', 'enum', ['optional']] ).to_json(),
        ['e1', None, ['a','b','c'], 'c', 'enum', ['optional']] )

with test.closure( 'r/o options are still settable' ):
    # NOTE: the DDS options do not enforce logic post initialization; they serve only to COMMUNICATE any state and limits
    test.check_equal( dds.option.from_json( ['1', 0, 'desc'] ).value_type(), 'int' )
    dds.option.from_json( ['1', 0, 'desc'] ).set_value( 20. )  # OK because 20.0 can be expressed as int
    test.check_throws( lambda:
        dds.option.from_json( ['1', 0, 'desc'] ).set_value( 20.5 ),
        RuntimeError, 'not convertible to a signed integer: 20.5' )

with test.closure( 'optional (default) value' ):
    test.check_false( dds.option.from_json( ['1', 0, 'desc'] ).is_optional() )
    test.check( dds.option.from_json( ['Asic Temperature',None,-40.0,125.0,0.0,0.0,'Current Asic Temperature (degree celsius)',['optional','read-only']] ).is_optional() )
    dds.option.from_json( ['4', 'string-value', None, 'desc', ['optional']] )
    dds.option.from_json( ['5', None, 'default-string-value', 'desc', ['optional']] )  # string type is deduced
    test.check_throws( lambda:
        dds.option.from_json( ['a', None, 'desc', ['optional']] ),
        RuntimeError, 'cannot deduce value type: ["a",null,"desc",["optional"]]' )
    test.check_equal_lists(
        dds.option.from_json( ['Integer Option', None, None, 'Something', ['optional', 'int']] ).to_json(),
        ['Integer Option', None, None, 'Something', ['optional', 'int']] )

with test.closure( 'mixed types' ):
    test.check_equal( dds.option.from_json( ['i', 0, 'desc'] ).value_type(), 'int' )
    test.check_equal( dds.option.from_json( ['f', 0, 'desc', ['float']] ).value_type(), 'float' )
    test.check_equal( dds.option.from_json( ['1', 0., 0, 1, 1, 0, 'desc'] ).value_type(), 'float' )
    test.check_equal( dds.option.from_json( ['2', 0, 0., 1, 1, 0, 'desc'] ).value_type(), 'float' )
    test.check_equal( dds.option.from_json( ['3', 0, 0, 1., 1, 0, 'desc'] ).value_type(), 'float' )
    test.check_equal( dds.option.from_json( ['4', 0, 0, 1, 1., 0, 'desc'] ).value_type(), 'float' )
    test.check_equal( dds.option.from_json( ['5', 0, 0, 1, 1, 0., 'desc'] ).value_type(), 'float' )
    test.check_equal( dds.option.from_json( ['-1', 0, -1, 4, 1, 0, 'desc'] ).value_type(), 'int' )
    test.check_equal(           # float, but forced to an int
        dds.option.from_json( ['3f', 3, 1, 5, 1, 2., '', ['int']] ).value_type(), 'int' )
    test.check_throws( lambda:  # same, but with an actual float
        dds.option.from_json( ['3f', 3, 1, 5, 1, 2.2, '', ['int']] ),
        RuntimeError, 'not convertible to a signed integer: 2.2' )

    dds.option.from_json( ['a', 9223372036854775807, 'desc'] )       # max int64
    test.check_throws( lambda:
        dds.option.from_json( ['a', 9223372036854775808, 'desc'] ),  # uint64
        RuntimeError, 'not convertible to a signed integer: 9223372036854775808' )
    dds.option.from_json( ['a', -9223372036854775808, 'desc'] )    # min int64
    # -9223372036854775809 cannot be converted to json

    test.check_equal( dds.option.from_json( ['Brightness',0,-64,64,1,0,'UVC image brightness'] ).value_type(), 'int' )

with test.closure( 'range' ):
    test.check_throws( lambda:
        dds.option.from_json( ['3x', 0, 0, -1, 1, 0, ''] ),
        RuntimeError, 'default value 0 > -1 maximum' )
    test.check_throws( lambda:
        dds.option.from_json( ['3x', 0, 2, 3, 1, 0, ''] ),
        RuntimeError, 'default value 0 < 2 minimum' )
    test.check_throws( lambda:
        dds.option.from_json( ['3y', -1, -2, 2, 1, 3, 'bad default'] ),
        RuntimeError, 'default value 3 > 2 maximum' )
    dds.option.from_json( ['3', -1, -2, 2, 1, 0, ''] )
    dds.option.from_json( ['Backlight Compensation', 0, 0, 1, 1, 0, 'Backlight custom description'] )
    dds.option.from_json( ['Custom Option', 5., -10, 10, 1, -5., 'Description'] )

with test.closure( 'IP address' ):
    dds.option.from_json( ['ip', None, 'desc', ['IPv4', 'optional']] )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: ""' )
    dds.option.from_json( ['ip', '0.0.0.0', 'desc', ['IPv4']] )
    dds.option.from_json( ['ip', '255.255.255.255', 'desc', ['IPv4']] )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '255.255.255.256', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "255.255.255.256"' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '255.255.256.255', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "255.255.256.255"' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '255.256.255.255', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "255.256.255.255"' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '256.255.255.255', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "256.255.255.255"' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '1.2.3.4a', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "1.2.3.4a"' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '1.2.3.4.', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "1.2.3.4."' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '1.2..4', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "1.2..4"' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '1.2.3.', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "1.2.3."' )
    test.check_throws( lambda:
        dds.option.from_json( ['ip', '1.2.3', 'desc', ['IPv4']] ),
        RuntimeError, 'not an IP address: "1.2.3"' )


#############################################################################################
test.print_results()