# -*- coding: utf-8 -*-

# Import python libs
from __future__ import absolute_import
import re

# Import salt testing libs
from salttesting.unit import TestCase
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')

from salt.modules import jboss7_cli
from salt.exceptions import CommandExecutionError


try:
    # will pass if executed along with other tests
    __salt__
except NameError:
    # if executed separately we need to export __salt__ dictionary ourselves
    from salt.ext.six.moves import builtins as __builtin__
    __builtin__.__salt__ = {}


class CmdMock(object):
    commands = []
    command_response_func = None     # if you want to test complete response object (with retcode, stdout and stderr)
    cli_commands = []

    default_response = {'retcode': 0, 'stdout': ''' {
        "outcome" => "success"
    }''', 'stderr': ''}

    def __init__(self, command_response_func=None):
        self.command_response_func = command_response_func

    def run_all(self, command):
        self.commands.append(command)
        if self.command_response_func is not None:
            return self.command_response_func(command)

        cli_command = self.__get_cli_command(command)
        self.cli_commands.append(cli_command)
        return self.default_response

    @staticmethod
    def __get_cli_command(command):
        command_re = re.compile(r'--command=\"\s*(.+?)\s*\"$', re.DOTALL)
        m = command_re.search(command)  # --command has to be the last argument
        if m:
            cli_command = m.group(1)
            return cli_command
        return None

    def get_last_command(self):
        if len(self.commands) > 0:
            return self.commands[-1]
        else:
            return None

    def get_last_cli_command(self):
        if len(self.cli_commands) > 0:
            return self.cli_commands[-1]
        else:
            return None

    def clear(self):
        self.commands = []
        self.command_response_func = None
        self.cli_commands = []


class JBoss7CliTestCase(TestCase):
    org_cmd_run_all = None
    cmd = CmdMock()
    jboss_config = {
        'cli_path': '/opt/jboss/jboss-eap-6.0.1/bin/jboss-cli.sh',
        'controller': '123.234.345.456:9999',
        'instance_name': 'Instance1',
        'cli_user': 'jbossadm',
        'cli_password': 'jbossadm',
        'status_url': 'http://sampleapp.example.com:8080/'
    }

    def setUp(self):
        self.cmd.clear()
        if 'cmd.run_all' in __salt__:
            self.org_cmd_run_all = __salt__['cmd.run_all']
        __salt__['cmd.run_all'] = self.cmd.run_all

    def tearDown(self):
        if self.org_cmd_run_all is not None:
            __salt__['cmd.run_all'] = self.org_cmd_run_all

    def test_controller_authentication(self):
        jboss7_cli.run_operation(self.jboss_config, 'some cli operation')

        self.assertEqual(self.cmd.get_last_command(), '/opt/jboss/jboss-eap-6.0.1/bin/jboss-cli.sh --connect --controller="123.234.345.456:9999" --user="jbossadm" --password="jbossadm" --command="some cli operation"')

    def test_controller_without_authentication(self):
        jboss_config = {
            'cli_path': '/opt/jboss/jboss-eap-6.0.1/bin/jboss-cli.sh',
            'controller': '123.234.345.456:9999'
        }
        jboss7_cli.run_operation(jboss_config, 'some cli operation')

        self.assertEqual(self.cmd.get_last_command(), '/opt/jboss/jboss-eap-6.0.1/bin/jboss-cli.sh --connect --controller="123.234.345.456:9999" --command="some cli operation"')

    def test_operation_execution(self):
        operation = r'sample_operation'
        jboss7_cli.run_operation(self.jboss_config, operation)

        self.assertEqual(self.cmd.get_last_command(), r'/opt/jboss/jboss-eap-6.0.1/bin/jboss-cli.sh --connect --controller="123.234.345.456:9999" --user="jbossadm" --password="jbossadm" --command="sample_operation"')

    def test_handling_jboss_error(self):
        def command_response(command):
            return {'retcode': 1,
                    'stdout': r'''{
                       "outcome" => "failed",
                       "failure-description" => "JBAS014807: Management resource '[
                       (\"subsystem\" => \"datasources\"),
                       (\"data-source\" => \"non-existing\")
                    ]' not found",
                        "rolled-back" => true,
                        "response-headers" => {"process-state" => "reload-required"}
                    }
                    ''',
                    'stderr': 'some err'}
        self.cmd.command_response_func = command_response

        result = jboss7_cli.run_operation(self.jboss_config, 'some cli command')

        self.assertFalse(result['success'])
        self.assertEqual(result['err_code'], 'JBAS014807')

    def test_handling_cmd_not_exists(self):
        def command_response(command):
            return {'retcode': 127,
                    'stdout': '''Command not exists''',
                    'stderr': 'some err'}
        self.cmd.command_response_func = command_response

        try:
            jboss7_cli.run_operation(self.jboss_config, 'some cli command')
            # should throw an exception
            assert False
        except CommandExecutionError as e:
            self.assertTrue(str(e).startswith('Could not execute jboss-cli.sh script'))

    def test_handling_other_cmd_error(self):
        def command_response(command):
            return {'retcode': 1,
                    'stdout': '''Command not exists''',
                    'stderr': 'some err'}
        self.cmd.command_response_func = command_response

        try:
            jboss7_cli.run_command(self.jboss_config, 'some cli command')
            # should throw an exception
            self.fail('An exception should be thrown')
        except CommandExecutionError as e:
            self.assertTrue(str(e).startswith('Command execution failed'))

    def test_matches_cli_output(self):
        text = '''{
            "key1" => "value1"
            "key2" => "value2"
            }
            '''

        self.assertTrue(jboss7_cli._is_cli_output(text))

    def test_not_matches_cli_output(self):
        text = '''Some error '''

        self.assertFalse(jboss7_cli._is_cli_output(text))

    def test_parse_flat_dictionary(self):
        text = '''{
            "key1" => "value1"
            "key2" => "value2"
            }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(len(result), 2)
        self.assertEqual(result['key1'], 'value1')
        self.assertEqual(result['key2'], 'value2')

    def test_parse_nested_dictionary(self):
        text = '''{
            "key1" => "value1",
            "key2" => {
                "nested_key1" => "nested_value1"
            }
        }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(len(result), 2)
        self.assertEqual(result['key1'], 'value1')
        self.assertEqual(len(result['key2']), 1)
        self.assertEqual(result['key2']['nested_key1'], 'nested_value1')

    def test_parse_string_after_dict(self):
        text = '''{
            "result" => {
                "jta" => true
            },
            "response-headers" => {"process-state" => "reload-required"}
        }'''

        result = jboss7_cli._parse(text)

        self.assertTrue(result['result']['jta'])
        self.assertEqual(result['response-headers']['process-state'], 'reload-required')

    def test_parse_all_datatypes(self):
        text = '''{
            "outcome" => "success",
            "result" => {
                "allocation-retry" => undefined,
                "connection-url" => "jdbc:mysql://localhost:3306/appdb",
                "driver-name" => "mysql",
                "enabled" => false,
                "jta" => true
            },
            "response-headers" => {"process-state" => "reload-required"}
        }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(result['outcome'], 'success')
        self.assertIsNone(result['result']['allocation-retry'])
        self.assertEqual(result['result']['connection-url'], 'jdbc:mysql://localhost:3306/appdb')
        self.assertEqual(result['result']['driver-name'], 'mysql')
        self.assertEqual(result['result']['enabled'], False)
        self.assertTrue(result['result']['jta'])
        self.assertEqual(result['response-headers']['process-state'], 'reload-required')

    def test_multiline_strings_with_escaped_quotes(self):
        text = r'''{
            "outcome" => "failed",
            "failure-description" => "JBAS014807: Management resource '[
            (\"subsystem\" => \"datasources\"),
            (\"data-source\" => \"asc\")
        ]' not found",
            "rolled-back" => true,
            "response-headers" => {"process-state" => "reload-required"}
        }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(result['outcome'], 'failed')
        self.assertTrue(result['rolled-back'])
        self.assertEqual(result['response-headers']['process-state'], 'reload-required')
        self.assertEqual(result['failure-description'], r'''JBAS014807: Management resource '[
            (\"subsystem\" => \"datasources\"),
            (\"data-source\" => \"asc\")
        ]' not found''')

    def test_handling_double_backslash_in_return_values(self):
        text = r'''{
                 "outcome" => "success",
                 "result" => {
                    "binding-type" => "simple",
                    "value" => "DOMAIN\\user"
                   }
                }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(result['outcome'], 'success')
        self.assertEqual(result['result']['binding-type'], 'simple')
        self.assertEqual(result['result']['value'], r'DOMAIN\user')

    def test_numbers_without_quotes(self):
        text = r'''{
                "outcome" => "success",
                "result" => {
                    "min-pool-size" => 1233,
                    "new-connection-sql" => undefined
                }
            }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(result['outcome'], 'success')
        self.assertEqual(result['result']['min-pool-size'], 1233)
        self.assertIsNone(result['result']['new-connection-sql'])

    def test_all_datasource_properties(self):
        text = r'''{
            "outcome" => "success",
            "result" => {
                "allocation-retry" => undefined,
                "allocation-retry-wait-millis" => undefined,
                "allow-multiple-users" => undefined,
                "background-validation" => undefined,
                "background-validation-millis" => undefined,
                "blocking-timeout-wait-millis" => undefined,
                "check-valid-connection-sql" => undefined,
                "connection-properties" => undefined,
                "connection-url" => "jdbc:mysql:thin:@db.example.com",
                "datasource-class" => undefined,
                "driver-class" => undefined,
                "driver-name" => "mysql",
                "enabled" => true,
                "exception-sorter-class-name" => undefined,
                "exception-sorter-properties" => undefined,
                "flush-strategy" => "FailingConnectionOnly",
                "idle-timeout-minutes" => undefined,
                "jndi-name" => "java:/appDS",
                "jta" => true,
                "max-pool-size" => 20,
                "min-pool-size" => 3,
                "new-connection-sql" => undefined,
                "password" => "Password4321",
                "pool-prefill" => undefined,
                "pool-use-strict-min" => undefined,
                "prepared-statements-cache-size" => undefined,
                "query-timeout" => undefined,
                "reauth-plugin-class-name" => undefined,
                "reauth-plugin-properties" => undefined,
                "security-domain" => undefined,
                "set-tx-query-timeout" => false,
                "share-prepared-statements" => false,
                "spy" => false,
                "stale-connection-checker-class-name" => undefined,
                "stale-connection-checker-properties" => undefined,
                "track-statements" => "NOWARN",
                "transaction-isolation" => undefined,
                "url-delimiter" => undefined,
                "url-selector-strategy-class-name" => undefined,
                "use-ccm" => "true",
                "use-fast-fail" => false,
                "use-java-context" => "false",
                "use-try-lock" => undefined,
                "user-name" => "user1",
                "valid-connection-checker-class-name" => undefined,
                "valid-connection-checker-properties" => undefined,
                "validate-on-match" => false,
                "statistics" => {
                    "jdbc" => undefined,
                    "pool" => undefined
                }
            },
            "response-headers" => {"process-state" => "reload-required"}
        }'''

        result = jboss7_cli._parse(text)

        self.assertEqual(result['outcome'], 'success')
        self.assertEqual(result['result']['max-pool-size'], 20)
        self.assertIsNone(result['result']['new-connection-sql'])
        self.assertIsNone(result['result']['url-delimiter'])
        self.assertFalse(result['result']['validate-on-match'])

    def test_datasource_resource_one_attribute_description(self):
        cli_output = '''{
            "outcome" => "success",
            "result" => {
                "description" => "A JDBC data-source configuration",
                "head-comment-allowed" => true,
                "tail-comment-allowed" => true,
                "attributes" => {
                    "connection-url" => {
                        "type" => STRING,
                        "description" => "The JDBC driver connection URL",
                        "expressions-allowed" => true,
                        "nillable" => false,
                        "min-length" => 1L,
                        "max-length" => 2147483647L,
                        "access-type" => "read-write",
                        "storage" => "configuration",
                        "restart-required" => "no-services"
                    }
                },
                "children" => {"connection-properties" => {"description" => "The connection-properties element allows you to pass in arbitrary connection properties to the Driver.connect(url, props) method"}}
            }
        }
        '''
        result = jboss7_cli._parse(cli_output)

        self.assertEqual(result['outcome'], 'success')
        conn_url_attributes = result['result']['attributes']['connection-url']
        self.assertEqual(conn_url_attributes['type'], 'STRING')
        self.assertEqual(conn_url_attributes['description'], 'The JDBC driver connection URL')
        self.assertTrue(conn_url_attributes['expressions-allowed'])
        self.assertFalse(conn_url_attributes['nillable'])
        self.assertEqual(conn_url_attributes['min-length'], 1)
        self.assertEqual(conn_url_attributes['max-length'], 2147483647)
        self.assertEqual(conn_url_attributes['access-type'], 'read-write')
        self.assertEqual(conn_url_attributes['storage'], 'configuration')
        self.assertEqual(conn_url_attributes['restart-required'], 'no-services')

    def test_datasource_complete_resource_description(self):
        cli_output = '''{
            "outcome" => "success",
            "result" => {
                "description" => "A JDBC data-source configuration",
                "head-comment-allowed" => true,
                "tail-comment-allowed" => true,
                "attributes" => {
                    "connection-url" => {
                        "type" => STRING,
                        "description" => "The JDBC driver connection URL",
                        "expressions-allowed" => true,
                        "nillable" => false,
                        "min-length" => 1L,
                        "max-length" => 2147483647L,
                        "access-type" => "read-write",
                        "storage" => "configuration",
                        "restart-required" => "no-services"
                    }
                },
                "children" => {"connection-properties" => {"description" => "The connection-properties element allows you to pass in arbitrary connection properties to the Driver.connect(url, props) method"}}
            }
        }
        '''

        result = jboss7_cli._parse(cli_output)

        self.assertEqual(result['outcome'], 'success')
        conn_url_attributes = result['result']['attributes']['connection-url']
        self.assertEqual(conn_url_attributes['type'], 'STRING')
        self.assertEqual(conn_url_attributes['description'], 'The JDBC driver connection URL')
        self.assertTrue(conn_url_attributes['expressions-allowed'])
        self.assertFalse(conn_url_attributes['nillable'])
        self.assertEqual(conn_url_attributes['min-length'], 1)
        self.assertEqual(conn_url_attributes['max-length'], 2147483647)
        self.assertEqual(conn_url_attributes['access-type'], 'read-write')
        self.assertEqual(conn_url_attributes['storage'], 'configuration')
        self.assertEqual(conn_url_attributes['restart-required'], 'no-services')

    def test_escaping_operation_with_backslashes_and_quotes(self):
        operation = r'/subsystem=naming/binding="java:/sampleapp/web-module/ldap/username":add(binding-type=simple, value="DOMAIN\\\\user")'
        jboss7_cli.run_operation(self.jboss_config, operation)

        self.assertEqual(self.cmd.get_last_command(), r'/opt/jboss/jboss-eap-6.0.1/bin/jboss-cli.sh --connect --controller="123.234.345.456:9999" --user="jbossadm" --password="jbossadm" --command="/subsystem=naming/binding=\"java:/sampleapp/web-module/ldap/username\":add(binding-type=simple, value=\"DOMAIN\\\\\\\\user\")"')
