import unittest
import mock
import logging
import json
import tests.resources.pikamock
import tests.main_test_script
import pika.exceptions
import pika
import tests.globalvar

import esgfpid.rabbit
from esgfpid.rabbit.exceptions import PIDServerException

LOGGER = logging.getLogger(__name__)
LOGGER.addHandler(logging.NullHandler())

# Test resources:
from resources.TESTVALUES import *
import resources.TESTVALUES as TESTHELPERS

# Skip slow tests?
import globalvar
if globalvar.QUICK_ONLY:
    print('Skipping slow tests in module "%s".' % __name__)


class RabbitConnectorTestCase(unittest.TestCase):

    slow_message = '\nRunning a slow test (avoid by using -ls flag).'


    def setUp(self):
        LOGGER.info('######## Next test (%s) ##########', __name__)
    
    def tearDown(self):
        LOGGER.info('#############################')

    #
    # Setup of the connection/channel:
    #

    def test_init_ok(self):

        # Test variables:
        nodemanager = TESTHELPERS.get_nodemanager()

        # Run code to be tested:
        testrabbit = esgfpid.rabbit.synchronous.SynchronousRabbitConnector(nodemanager)

        # Check results:
        # Instance is created:
        self.assertIsInstance(testrabbit, esgfpid.rabbit.synchronous.SynchronousRabbitConnector)
        # Communcation not established yet:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertFalse(established)

    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_open_rabbit_connection_ok(self, connectionmock):

        # Define replacement for mocked method:
        # Using side_effect instead of return_value, so we can use the params object passed as arg!
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        
        # Run code to be tested (during this, the connection is opened, i.e. the patch is needed)
        testrabbit.open_rabbit_connection()

        # Check result:
        # Connection created:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertIsInstance(conn, tests.resources.pikamock.MockPikaBlockingConnection)
        # Channel created:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertIsInstance(channel, tests.resources.pikamock.MockChannel)
        # Communcation established:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertTrue(established)
        # Connected to preferred URL:
        expected_host = RABBIT_URL_TRUSTED
        self.assertIn(conn.host, expected_host, 'Connected to: %s instead of %s' % (conn.host, expected_host))


    @unittest.skipIf(globalvar.QUICK_ONLY, '(this test is slow)')
    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_open_rabbit_connection_not_ok(self, connectionmock):

        print(self.slow_message)

        # Define replacement for mocked method:
        # All connection attempts fail with an Authentication Error.
        connectionmock.side_effect = pika.exceptions.AuthenticationError

        # Make test rabbit:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        
        # Run code to be tested (during this, the connection is opened, i.e. the patch is needed)
        with self.assertRaises(PIDServerException) as e:
            testrabbit.open_rabbit_connection()

        # Check result:
        # Connection is None:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertIsNone(conn)
        # Channel is None:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertIsNone(channel)
        # Communcation not established:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertFalse(established)


    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_open_rabbit_connection_first_fails_auth(self, connectionmock):

        # Define replacement for mocked object:
        # RabbitMQ with priority 1 fails, the one with
        # priority 2 succeeds.
        def make_mocked_connection(params):
            if params.host == RABBIT_URL_TRUSTED:
                raise pika.exceptions.ProbableAuthenticationError
            elif params.host == RABBIT_URL_TRUSTED2:
                return tests.resources.pikamock.MockPikaBlockingConnection(params)
            else:
                raise ValueError("Unexpected RabbitMQ URL. Unit tests cannot work properly if the wrong test data is passed.")
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()

        # Run code to be tested (during this, the connection is opened, i.e. the patch is needed)
        testrabbit.open_rabbit_connection()

        # Check result:
        # Connection created:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertIsInstance(conn, tests.resources.pikamock.MockPikaBlockingConnection)
        # Channel created:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertIsInstance(channel, tests.resources.pikamock.MockChannel)
        # Communcation established:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertTrue(established)
        # Connected to fallback URL:
        expected_host = RABBIT_URL_TRUSTED2
        self.assertIn(conn.host, expected_host, 'Connected to: %s instead of %s' % (conn.host, expected_host))


    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_open_rabbit_connection_first_fails_conn(self, connectionmock):

        # Define replacement for mocked method:
        # RabbitMQ with priority 1 fails, the one with
        # priority 2 succeeds.
        def make_mocked_connection(params):
            if params.host == RABBIT_URL_TRUSTED:
                raise pika.exceptions.AMQPConnectionError
            elif params.host == RABBIT_URL_TRUSTED2:
                return tests.resources.pikamock.MockPikaBlockingConnection(params)
            else:
                raise ValueError("Unexpected RabbitMQ URL. Unit tests cannot work properly if the wrong test data is passed.")
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()

        # Run code to be tested (during this, the connection is opened, i.e. the patch is needed)
        testrabbit.open_rabbit_connection()

        # Check result:
        # Connection created:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertIsInstance(conn, tests.resources.pikamock.MockPikaBlockingConnection)
        # Channel created:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertIsInstance(channel, tests.resources.pikamock.MockChannel)
        # Communcation established:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertTrue(established)
        # Connected to fallback URL:
        expected_host = RABBIT_URL_TRUSTED2
        self.assertIn(conn.host, expected_host, 'Connected to: %s instead of %s' % (conn.host, expected_host))

    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_open_rabbit_connection_first_two_fail(self, connectionmock):

        # Define replacement for mocked method:
        # RabbitMQ with priority 1&2 fail, the third one succeeds.
        def make_mocked_connection(params):
            if params.host == RABBIT_URL_TRUSTED or params.host == RABBIT_URL_TRUSTED2:
                raise pika.exceptions.AMQPConnectionError
            elif params.host == RABBIT_URL_TRUSTED3:
                return tests.resources.pikamock.MockPikaBlockingConnection(params)
            else:
                raise ValueError("Unexpected RabbitMQ URL. Unit tests cannot work properly if the wrong test data is passed.")
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()

        # Run code to be tested (during this, the connection is opened, i.e. the patch is needed)
        testrabbit.open_rabbit_connection()

        # Check result:
        # Connection created:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertIsInstance(conn, tests.resources.pikamock.MockPikaBlockingConnection)
        # Channel created:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertIsInstance(channel, tests.resources.pikamock.MockChannel)
        # Communcation established:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertTrue(established)
        # Connected to fallback URL:
        expected_host = RABBIT_URL_TRUSTED3
        self.assertIn(conn.host, expected_host, 'Connected to: %s instead of %s' % (conn.host, expected_host))

    @unittest.skipIf(globalvar.QUICK_ONLY, '(this test is slow)')
    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_channel')
    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_open_rabbit_channel_fails(self, connectionmock, channelmock):

        print(self.slow_message)

        # Define replacement for mocked methods:
        # Connection succeeds, channel does not.
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection
        channelmock.side_effect = pika.exceptions.ChannelClosed(reply_code=0, reply_text='Channel Closed')

        # Make test rabbit:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()

        # Run code to be tested (during this, the connection is opened, i.e. the patch is needed)
        with self.assertRaises(PIDServerException) as e:
            testrabbit.open_rabbit_connection()

        # Check result:
        # Connection created:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertIsInstance(conn, tests.resources.pikamock.MockPikaBlockingConnection)
        # Channel is None:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertIsNone(channel)
        # Communcation NOT established:
        established = testrabbit._SynchronousRabbitConnector__communication_established
        self.assertFalse(established)
        # Connected to fallback URL:
        expected_host = RABBIT_URL_TRUSTED3
        self.assertIn(conn.host, expected_host, 'Connected to: %s instead of %s' % (conn.host, expected_host))

    #
    # Close connection
    #

    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_close_rabbit_connection_ok(self, connectionmock):

        # Define replacement for mocked object:
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit with open connection:
        #(during this, the connection is opened, i.e. the patch is needed)
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        testrabbit.open_rabbit_connection()

        # Run code to be tested:
        testrabbit.close_rabbit_connection()

        # Check result:
        # Connection should be closed:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertFalse(conn.is_open)

    #
    # Send message
    #

    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_send_message_ok(self, connectionmock):
        '''
        We expect the message to arrive in the MockChannel
        on the first try.
        '''

        # Test variables:
        key = 'mykey'
        msg = {"foo":"bar", "ROUTING_KEY":key}

        # Define replacement for mocked method:
        # A mocked connection is created.
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit with connection:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        testrabbit.open_rabbit_connection()

        # Run code to be tested:
        testrabbit.send_message_to_queue(msg)

        # Check result:
        # Message was sent:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertEqual(channel.success_counter, 1, 'Message should succeed!')
        # Message was sent on first try:
        self.assertEqual(channel.publish_counter, 1,
            'Message was sent %i times!' % channel.publish_counter)
        # Message arrived in mock channel:
        self.assertIn(json.dumps(msg), channel.messages,
            'Message %s not found in channel\'s list: %s' % (msg, channel.messages))
        self.assertIn(key, channel.routing_keys,
            'Routing key %s not found in channel\'s list: %s' % (key, channel.routing_keys))

    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_send_message_fail(self, connectionmock):

        # Test variables:
        key = 'mykey'
        msg = {"foo":"bar", "ROUTING_KEY":key}

        # Define replacement for mocked method:
        # A mocked connection is created.
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit with connection:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        testrabbit.open_rabbit_connection()

        # Make publish raise an UnroutableError
        channel = testrabbit._SynchronousRabbitConnector__channel
        channel.num_failures = float('inf') # always failure!

        # Run code to be tested:
        with self.assertRaises(esgfpid.exceptions.MessageNotDeliveredException) as e:
            testrabbit.send_message_to_queue(msg)

        # Check result:
        self.assertEqual(channel.success_counter, 0, 'Message should not succeed!')

    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_send_message_unroutable(self, connectionmock):

        # Test variables:
        key = 'mykey'
        msg = {"foo":"bar", "ROUTING_KEY":key}

        # Define replacement for mocked object:
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection
        # Make basic_publish raise an error
        #publishmock.side_effect = pika.exceptions.UnroutableError('foo bar')

        # Make test rabbit with connection:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        testrabbit.open_rabbit_connection()

        # Make publish raise an UnroutableError
        channel = testrabbit._SynchronousRabbitConnector__channel
        channel.num_unroutables = float('inf') # always unroutable!

        # Run code to be tested:
        with self.assertRaises(esgfpid.exceptions.MessageNotDeliveredException) as e:
            testrabbit.send_message_to_queue(msg)

        # Check result:
        self.assertEqual(channel.success_counter, 0, 'Message should not succeed!')
        self.assertEqual(channel.publish_counter, 2, 'Message was tried %i times, not 2.' % channel.publish_counter)



    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_send_message_first_fails_then_ok(self, connectionmock):
        '''
        We make the channel return False (failure), then True (success).
        We check if the message arrived in the MockChannel.
        '''

        # Test variables:
        key = 'mykey'
        msg = {"foo":"bar", "ROUTING_KEY":key}

        # Define replacement for mocked object:
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit with connection:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()
        testrabbit.open_rabbit_connection()

        # Make publish succeed only in 2nd try:
        channel = testrabbit._SynchronousRabbitConnector__channel
        channel.num_failures = 1

        # Run code to be tested:
        testrabbit.send_message_to_queue(msg)

        # Check result:
        # Message was sent:
        self.assertEqual(channel.success_counter, 1, 'Message should succeed!')
        # Message was sent on second try:
        self.assertEqual(channel.publish_counter, 2,
            'Message was sent %i times!' % channel.publish_counter)
        # Message arrived in mock channel:
        self.assertIn(json.dumps(msg), channel.messages,
            'Message %s not found in channel\'s list: %s' % (msg, channel.messages))
        self.assertIn(key, channel.routing_keys,
            'Routing key %s not found in channel\'s list: %s' % (key, channel.routing_keys))


    @mock.patch('esgfpid.rabbit.synchronous.SynchronousRabbitConnector._SynchronousRabbitConnector__make_connection')
    def test_send_message_no_connection(self, connectionmock):
        '''
        If there is no connection, it is built before publishing
        the message.
        We check if the message arrived in the MockChannel.
        '''

        # Test variables:
        key = 'mykey'
        msg = {"foo":"bar", "ROUTING_KEY":key}

        # Define replacement for mocked object:
        def make_mocked_connection(params):
            return tests.resources.pikamock.MockPikaBlockingConnection(params)
        connectionmock.side_effect = make_mocked_connection

        # Make test rabbit without connection:
        testrabbit = TESTHELPERS.get_synchronous_rabbit()

        # Run code to be tested:
        testrabbit.send_message_to_queue(msg)

        # Check result:
        # Connection is opened:
        conn = testrabbit._SynchronousRabbitConnector__connection
        self.assertTrue(conn.is_open)
        # Message was sent:
        channel = testrabbit._SynchronousRabbitConnector__channel
        self.assertEqual(channel.success_counter, 1, 'Message should succeed!')
        # Message was sent on first try:
        self.assertEqual(channel.publish_counter, 1,
            'Message was sent %i times!' % channel.publish_counter)
        # Message arrived in mock channel:
        self.assertIn(json.dumps(msg), channel.messages,
            'Message %s not found in channel\'s list: %s' % (msg, channel.messages))
        self.assertIn(key, channel.routing_keys,
            'Routing key %s not found in channel\'s list: %s' % (key, channel.routing_keys))