from pprint import pformat
from unittest import TestCase
from unittest.mock import MagicMock, patch, call

from ibind.base.rest_client import Result
from ibind.client.ibkr_utils import StockQuery, filter_stocks, find_answer, QuestionType, handle_questions, question_type_to_message_id, OrderRequest, parse_order_request
from ibind.support.logs import project_logger
from test.integration.client import ibkr_responses
from test_utils import verify_log


class TestIbkrUtilsI(TestCase):
    def setUp(self):
        self.instruments = ibkr_responses.responses['stocks']
        self.result = Result(data=self.instruments)
        self.maxDiff = None

    def test_filter_stocks(self):
        queries = [
            StockQuery(symbol='AAPL', contract_conditions={'isUS': False}, name_match='APPLE'),
            StockQuery(symbol='BBVA', contract_conditions={'exchange': 'NYSE'}),
            StockQuery(symbol='CDN', contract_conditions={'isUS': True}),
            StockQuery(symbol='CFC', contract_conditions={}),
            StockQuery(symbol='GOOG', contract_conditions={'isUS': False}, instrument_conditions={'chineseName': 'Alphabet&#x516C;&#x53F8;'}),
            'HUBS',
            StockQuery(symbol='META', name_match='meta ', contract_conditions={'isUS': False}, instrument_conditions={}),
            StockQuery(symbol='MSFT', contract_conditions={'exchange': 'NASDAQ'}),
            StockQuery(symbol='SAN', name_match='SANTANDER'),
            StockQuery(symbol='SCHW', contract_conditions={'exchange': 'NASDAQ'}),
            StockQuery(symbol='TEAM', name_match='ATLASSIAN'),
            StockQuery(symbol='INVALID_SYMBOL')
        ]  # fmt: skip
        with self.assertLogs(project_logger(), level='INFO') as cm:
            rv = filter_stocks(queries, Result(data=self.instruments), default_filtering=False)

        verify_log(
            self, cm, [f'Error getting stocks. Could not find valid instruments INVALID_SYMBOL in result: {self.result}. Skipping query={queries[-1]}.']
        )  # fmt: skip

        # pprint(rv)

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x82F9;&#x679C;&#x516C;&#x53F8;',
                    'contracts': [
                        {'conid': 38708077, 'exchange': 'MEXI', 'isUS': False},
                        {'conid': 273982664, 'exchange': 'EBS', 'isUS': False},
                    ],
                    'name': 'APPLE INC',
                },
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x82F9;&#x679C;&#x516C;&#x53F8;',
                    'contracts': [{'conid': 532640894, 'exchange': 'AEQLIT', 'isUS': False}],
                    'name': 'APPLE INC-CDR',
                },
            ],
            rv.data['AAPL'],
        )

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x897F;&#x73ED;&#x7259;&#x5BF9;&#x5916;&#x94F6;&#x884C;',
                    'contracts': [{'conid': 4815, 'exchange': 'NYSE', 'isUS': True}],
                    'name': 'BANCO BILBAO VIZCAYA-SP ADR',
                },
            ],
            rv.data['BBVA'],
        )

        self.assertEqual([], rv.data['CDN'])

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': None,
                    'contracts': [{'conid': 42001300, 'exchange': 'IBIS', 'isUS': False}],
                    'name': 'UET UNITED ELECTRONIC TECHNO',
                }
            ],
            rv.data['CFC'],
        )

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': 'Alphabet&#x516C;&#x53F8;',
                    'contracts': [
                        {'conid': 210810667, 'exchange': 'MEXI', 'isUS': False},
                    ],
                    'name': 'ALPHABET INC-CL C',
                },
                {
                    'assetClass': 'STK',
                    'chineseName': 'Alphabet&#x516C;&#x53F8;',
                    'contracts': [{'conid': 532638805, 'exchange': 'AEQLIT', 'isUS': False}],
                    'name': 'ALPHABET INC - CDR',
                },
            ],
            rv.data['GOOG'],
        )

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': 'HubSpot&#x516C;&#x53F8;',
                    'contracts': [{'conid': 169544810, 'exchange': 'NYSE', 'isUS': True}],
                    'name': 'HUBSPOT INC',
                }
            ],
            rv.data['HUBS'],
        )

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': 'Meta&#x5E73;&#x53F0;&#x80A1;&#x4EFD;&#x6709;&#x9650;&#x516C;&#x53F8;',
                    'contracts': [
                        {'conid': 114922621, 'exchange': 'MEXI', 'isUS': False},
                    ],
                    'name': 'META PLATFORMS INC-CLASS A',
                },
                {
                    'assetClass': 'STK',
                    'chineseName': 'Meta&#x5E73;&#x53F0;&#x80A1;&#x4EFD;&#x6709;&#x9650;&#x516C;&#x53F8;',
                    'contracts': [{'conid': 530091499, 'exchange': 'AEQLIT', 'isUS': False}],
                    'name': 'META PLATFORMS INC-CDR',
                },
            ],
            rv.data['META'],
        )

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x5FAE;&#x8F6F;&#x516C;&#x53F8;',
                    'contracts': [
                        {'conid': 272093, 'exchange': 'NASDAQ', 'isUS': True},
                    ],
                    'name': 'MICROSOFT CORP',
                },
            ],
            rv.data['MSFT'],
        )

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x6851;&#x5766;&#x5FB7;',
                    'contracts': [
                        {'conid': 38708867, 'exchange': 'MEXI', 'isUS': False},
                        {'conid': 385055564, 'exchange': 'WSE', 'isUS': False},
                    ],
                    'name': 'BANCO SANTANDER SA',
                },
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x6851;&#x5766;&#x5FB7;',
                    'contracts': [{'conid': 12442, 'exchange': 'NYSE', 'isUS': True}],
                    'name': 'BANCO SANTANDER SA-SPON ADR',
                },
                {
                    'assetClass': 'STK',
                    'chineseName': '&#x6851;&#x5766;&#x5FB7;&#x82F1;&#x56FD;&#x516C;&#x5171;&#x6709;&#x9650;&#x516C;&#x53F8;',
                    'contracts': [{'conid': 80993135, 'exchange': 'LSE', 'isUS': False}],
                    'name': 'SANTANDER UK PLC',
                },
            ],
            rv.data['SAN'],
        )

        self.assertEqual([], rv.data['SCHW'])

        self.assertEqual(
            [
                {
                    'assetClass': 'STK',
                    'chineseName': None,
                    'contracts': [{'conid': 589316251, 'exchange': 'NASDAQ', 'isUS': True}],
                    'name': 'ATLASSIAN CORP-CL A',
                },
            ],
            rv.data['TEAM'],
        )

    def test_question_type_to_message_id_successful(self):
        question_type = QuestionType.PRICE_PERCENTAGE_CONSTRAINT
        message_id = question_type_to_message_id(question_type)
        self.assertEqual(message_id, 'o163')


class TestFindAnswer(TestCase):
    def setUp(self):
        # Setup Answers dictionary here
        self.answers = {QuestionType.PRICE_PERCENTAGE_CONSTRAINT: True}

    def test_valid_question(self):
        question = f'Some {QuestionType.PRICE_PERCENTAGE_CONSTRAINT} specific question'
        answer = find_answer(question, self.answers)
        self.assertTrue(answer)

    def test_invalid_question(self):
        question = 'Nonexistent question type'
        with self.assertRaises(ValueError):
            find_answer(question, self.answers)


class TestHandleQuestionsI(TestCase):
    def setUp(self):
        self.original_result = Result(
            data=[{'id': '12345', 'message': ['price exceeds the Percentage constraint of 3%.']}], request={'url': 'test_url'}
        )
        self.answers = {QuestionType.PRICE_PERCENTAGE_CONSTRAINT: True}
        self.reply_callback = MagicMock()

    @patch('ibind.client.ibkr_utils.QuestionType')
    def test_successful_handling(self, question_type_mock):
        # Mocking the QuestionType enum
        question_type_mock.PRICE_PERCENTAGE_CONSTRAINT.__str__.return_value = 'price exceeds the Percentage constraint of 3%.'
        question_type_mock.ADDITIONAL_QUESTION_TYPE.__str__.return_value = 'This is an additional question.'

        self.answers = {question_type_mock.PRICE_PERCENTAGE_CONSTRAINT: True, question_type_mock.ADDITIONAL_QUESTION_TYPE: True}

        # Mock reply_callback to simulate the sequence of question-answer interactions
        replies = [
            Result(data=[{'id': '12346', 'message': ['This is an additional question.']}], request={'url': 'another_question_url'}),
            Result(data=[{'id': '12347'}], request={'url': 'final_url'}),  # No more questions
        ]
        self.reply_callback.side_effect = replies

        result = handle_questions(self.original_result, self.answers, self.reply_callback)
        self.assertEqual(result.request['url'], self.original_result.request['url'])
        self.assertEqual(len(self.reply_callback.call_args_list), 2)
        # Expected calls to self.reply_callback
        expected_calls = [
            call(
                self.original_result.data[0]['id'], self.answers[question_type_mock.PRICE_PERCENTAGE_CONSTRAINT]
            ),  # First call with question ID '12346' and reply True
            call(
                replies[0].data[0]['id'], self.answers[question_type_mock.ADDITIONAL_QUESTION_TYPE]
            ),  # Second call with question ID '12347' and reply True
        ]

        # Check if the calls to self.reply_callback are as expected
        self.assertEqual(expected_calls, self.reply_callback.call_args_list)

    def test_too_many_questions(self):
        # Simulate repetitive questions to exceed the question limit
        self.reply_callback.side_effect = [self.original_result] * 21

        with self.assertRaises(RuntimeError) as cm_err:
            handle_questions(self.original_result, self.answers, self.reply_callback)

        self.assertIn('Too many questions', str(cm_err.exception))

    def test_negative_reply(self):
        # Set a negative answer
        self.answers[QuestionType.PRICE_PERCENTAGE_CONSTRAINT] = False

        with self.assertRaises(RuntimeError) as cm_err:
            handle_questions(self.original_result, self.answers, self.reply_callback)
        self.assertEqual(
            f'A question was not given a positive reply. Question: "{self.original_result.data[0]["message"][0]}". Answers: \n{self.answers}\n. Request: {self.original_result.request}',
            str(cm_err.exception),
        )

    def test_multiple_orders_returned(self):
        # Simulate multiple orders in the data
        self.original_result.data = [
            {'id': '12345', 'message': [str(QuestionType.PRICE_PERCENTAGE_CONSTRAINT)]},
            {'id': '12346', 'message': [str(QuestionType.PRICE_PERCENTAGE_CONSTRAINT)]},
        ]
        self.reply_callback.return_value = self.original_result.copy(data=[{}])

        with self.assertLogs(project_logger(), level='INFO') as cm:
            handle_questions(self.original_result, self.answers, self.reply_callback)

        verify_log(self, cm, ['While handling questions multiple orders were returned: ' + pformat(self.original_result.data)])

    def test_multiple_messages_returned(self):
        # Simulate a single order with multiple messages
        self.original_result.data = [{'id': '12345', 'message': [str(QuestionType.PRICE_PERCENTAGE_CONSTRAINT), 'Message 2']}]
        self.reply_callback.return_value = self.original_result.copy(data=[{}])

        with self.assertLogs(project_logger(), level='INFO') as cm:
            handle_questions(self.original_result, self.answers, self.reply_callback)

        verify_log(self, cm, ['While handling questions multiple messages were returned: ' + pformat(self.original_result.data[0]['message'])])

class TestParseOrderRequestI(TestCase):
    def test_parse_both_with_conidex(self):
        order_request = OrderRequest(
            conid=None,
            side='BUY',
            quantity=321,
            order_type='MKT',
            acct_id='DU1234567',
            conidex='33333' # should cause exception
        )

        d = parse_order_request(order_request)

        self.assertEqual({
            'side': 'BUY',
            'quantity': 321,
            'orderType': 'MKT',
            'acctId': 'DU1234567',
            'conidex': '33333',
            'tif': 'GTC'
        }, d)

    def test_raise_with_conid_and_conidex(self):
        with self.assertRaises(ValueError) as cm_err:
            order_request = OrderRequest(
                conid=123,
                side='BUY',
                quantity=321,
                order_type='MKT',
                acct_id='DU1234567',
                conidex='33333' # should cause exception
            )

            parse_order_request(order_request)

        self.assertEqual("Both 'conidex' and 'conid' are provided. When using 'conidex', specify `conid=None`.", str(cm_err.exception))


