import asyncio
import datetime
from decimal import Decimal
from typing import List
from unittest.mock import AsyncMock, MagicMock, patch
import unittest
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
from optrabot.broker.tastytradeconnector import TastytradeConnector
from optrabot.broker.order import Leg, OptionRight, Order as GenericOrder, OrderAction, OrderType, PriceEffect

class TastytradeTests(unittest.TestCase):
	def setUp(self):
		self._cut = TastytradeConnector()

	def test_transform_generic_order(self):
		"""
		Test the transformation of a generic order to a Tastytrade order.
		The stop_trigger price of the transformed stop order must be rounded to 2 decimal places.
		The price of the transformed limit order must be rounded to 2 decimal places.
		"""
		symbol = 'SPX'
		expiration_date = datetime.datetime.today().date()

		# Test the transformation of a stop order
		leg1 = Leg(action=OrderAction.SELL_TO_CLOSE, strike=5500.0, symbol=symbol, right=OptionRight.CALL, expiration=expiration_date, quantity=1)
		leg1.midPrice = 1.2334433
		legs: List[Leg] = [leg1]
		#legs.append(Leg(action=OrderAction.SELL_TO_CLOSE, strike=5500.0, symbol=symbol, right=OptionRight.CALL, expiration=expiration_date, quantity=1))
		stop_order = GenericOrder(
			symbol=symbol, 
			legs=legs, 
			action=OrderAction.SELL_TO_CLOSE,
			quantity=1,
			type=OrderType.STOP,
			price=1.2334433
		)
		stop_order.determine_price_effect()
		self.assertEqual(stop_order.price_effect, PriceEffect.CREDIT)
		combolegs: List[Leg] = []
		stop_order.brokerSpecific['comboLegs'] = combolegs

		tasty_stop_order = self._cut._transform_generic_order(stop_order)
		expected_decimal = round(Decimal(1.23),2)
		self.assertEqual(tasty_stop_order.stop_trigger, expected_decimal)

		# Test the transformation of a limit order
		leg1 = Leg(action=OrderAction.BUY_TO_OPEN, strike=5500.0, symbol=symbol, right=OptionRight.CALL, expiration=expiration_date, quantity=1)
		leg1.midPrice = 1.2334433
		legs = [leg1]
		limit_order = GenericOrder(
			symbol=symbol,
			legs=legs,
			action=OrderAction.BUY_TO_OPEN,
			quantity=1,
			type=OrderType.LIMIT,
			price=1.2334433
		)
		limit_order.determine_price_effect()
		combolegs: List[Leg] = []
		limit_order.brokerSpecific['comboLegs'] = combolegs
		self.assertEqual(limit_order.price_effect, PriceEffect.DEBIT)
		tasty_limit_order = self._cut._transform_generic_order(limit_order)
		expected_decimal = round(Decimal(-1.23),2)
		self.assertEqual(tasty_limit_order.price, expected_decimal)

	def test_alert_streamer_reconnect_on_disconnect(self):
		"""
		Test that the Alert Streamer automatically reconnects when it disconnects unexpectedly.
		This test verifies the fix for OTB-264.
		"""
		# Create a mock alert streamer
		mock_alert_streamer = MagicMock()
		mock_alert_streamer.close = AsyncMock()
		
		# Create a mock task for listening to accounts
		mock_task = MagicMock()
		mock_task.done.return_value = False
		mock_task.cancel = MagicMock()
		
		# Setup the connector
		self._cut._alert_streamer = mock_alert_streamer
		self._cut.task_listen_accounts = mock_task
		self._cut._is_disconnecting = False
		self._cut._streamer = MagicMock()  # Must be set to prevent early return
		
		# Mock the _request_account_updates method to prevent actual connection
		async def mock_request_account_updates():
			# Simulate the task running
			await asyncio.sleep(0.1)
		
		with patch.object(self._cut, '_request_account_updates', side_effect=mock_request_account_updates):
			# Run the disconnect handler
			asyncio.run(self._cut._on_alert_streamer_disconnect(mock_alert_streamer))
			
			# Verify that the old task was cancelled
			mock_task.cancel.assert_called_once()
			
			# Verify that the old alert streamer was closed
			mock_alert_streamer.close.assert_called_once()
			
			# Verify that a new task was created
			self.assertIsNotNone(self._cut.task_listen_accounts)
			self.assertNotEqual(self._cut.task_listen_accounts, mock_task)

	def test_alert_streamer_no_reconnect_during_manual_disconnect(self):
		"""
		Test that the Alert Streamer does NOT reconnect when disconnecting manually.
		The _is_disconnecting flag should prevent automatic reconnection.
		"""
		# Create a mock alert streamer
		mock_alert_streamer = MagicMock()
		mock_alert_streamer.close = AsyncMock()
		
		# Create a mock task
		mock_task = MagicMock()
		mock_task.done.return_value = False
		mock_task.cancel = MagicMock()
		
		# Setup the connector with _is_disconnecting = True (manual disconnect)
		self._cut._alert_streamer = mock_alert_streamer
		self._cut.task_listen_accounts = mock_task
		self._cut._is_disconnecting = True
		
		# Run the disconnect handler
		asyncio.run(self._cut._on_alert_streamer_disconnect(mock_alert_streamer))
		
		# Verify that the task was NOT cancelled (no reconnect logic executed)
		mock_task.cancel.assert_not_called()
		
		# Verify that the alert streamer was NOT closed
		mock_alert_streamer.close.assert_not_called()

	def test_alert_streamer_reconnect_with_already_done_task(self):
		"""
		Test that the Alert Streamer reconnects correctly when the old task is already done.
		"""
		# Create a mock alert streamer
		mock_alert_streamer = MagicMock()
		mock_alert_streamer.close = AsyncMock()
		
		# Create a mock task that is already done
		mock_task = MagicMock()
		mock_task.done.return_value = True  # Task is already finished
		
		# Setup the connector
		self._cut._alert_streamer = mock_alert_streamer
		self._cut.task_listen_accounts = mock_task
		self._cut._is_disconnecting = False
		self._cut._streamer = MagicMock()  # Must be set to prevent early return
		
		# Mock the _request_account_updates method
		async def mock_request_account_updates():
			await asyncio.sleep(0.1)
		
		with patch.object(self._cut, '_request_account_updates', side_effect=mock_request_account_updates):
			# Run the disconnect handler
			asyncio.run(self._cut._on_alert_streamer_disconnect(mock_alert_streamer))
			
			# Verify that cancel was NOT called (task already done)
			mock_task.cancel.assert_not_called()
			
			# Verify that the old alert streamer was still closed
			mock_alert_streamer.close.assert_called_once()
			
			# Verify that a new task was created
			self.assertIsNotNone(self._cut.task_listen_accounts)
