import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import unittest
import asyncio
from datetime import datetime
from unittest.mock import Mock, patch, AsyncMock, MagicMock, call
from optrabot.trademanager import TradeManager
from optrabot.managedtrade import ManagedTrade
from optrabot.tradestatus import TradeStatus
from optrabot.broker.order import Order, OrderStatus, OrderType, OrderAction
from optrabot.tradetemplate.templatefactory import Template
from optrabot import models, schemas

class TradeManagerTests(unittest.TestCase):
	"""
	Unit tests for TradeManager class.
	Focus: Duplicate FILLED event handling and flow event emission.
	"""
	
	def setUp(self):
		"""Set up test fixtures"""
		# Reset singleton instance for each test
		TradeManager._instances = {}
		
	def tearDown(self):
		"""Clean up after tests"""
		# Shutdown TradeManager if it was created
		if TradeManager in TradeManager._instances:
			try:
				TradeManager._instances[TradeManager].shutdown()
			except:
				pass
		TradeManager._instances = {}
	
	def test_duplicate_filled_event_closing_order_ignored(self):
		"""
		OTB-XXX: Test that FILLED event after execution details is handled correctly.
		
		Scenario:
		- Multi-contract BAG order (Iron Condor with AMOUNT=3)
		- _onOrderExecutionDetailsEvent sets trade status to CLOSED (happens first)
		- Then _onOrderStatusChanged with FILLED is called
		- Should still send notifications and emit flow event
		- But should skip _close_trade() call (trade already closed)
		
		Expected:
		- _close_trade() is NOT called (trade already closed by execution details)
		- Notifications ARE sent
		- Flow event IS emitted
		- Tracking job IS removed
		- Debug log confirms skipping _close_trade()
		"""
		# Create mock managed trade
		mock_trade = Mock(spec=models.Trade)
		mock_trade.id = 691
		mock_trade.status = TradeStatus.OPEN
		
		mock_template = Mock(spec=Template)
		mock_template.name = "FINALIC100Income"
		mock_template.strategy = "Final IC 100 Income"
		mock_template.account = "DU6504444"
		
		# Create mock entry order
		entry_order = Mock(spec=Order)
		entry_order.broker_order_id = '132200'
		entry_order.status = OrderStatus.FILLED
		entry_order.averageFillPrice = 0.60
		
		managed_trade = ManagedTrade(mock_trade, mock_template, entry_order, "DU6504444")
		# Simulate that execution details event already closed the trade
		managed_trade.status = TradeStatus.CLOSED
		
		# Create mock closing order
		closing_order = Mock(spec=Order)
		closing_order.broker_order_id = '132218'
		closing_order.status = OrderStatus.OPEN
		closing_order.averageFillPrice = -0.9
		closing_order.symbol = 'SPX'
		managed_trade.closing_order = closing_order
		
		# Patch TradeManager methods and dependencies
		with patch('optrabot.trademanager.Session') as mock_session, \
		     patch('optrabot.trademanager.get_db_engine') as mock_engine, \
		     patch('optrabot.trademanager.crud') as mock_crud, \
		     patch('optrabot.trademanager.BrokerFactory') as mock_broker_factory, \
		     patch('optrabot.tradinghubclient.TradinghubClient') as mock_trading_hub, \
		     patch('optrabot.trademanager.AsyncIOScheduler') as mock_scheduler, \
		     patch('optrabot.trademanager.logger') as mock_logger:
			
			# Setup mocks
			mock_session_instance = MagicMock()
			mock_session.return_value.__enter__.return_value = mock_session_instance
			
			mock_db_trade = Mock(spec=models.Trade)
			mock_db_trade.id = 691
			mock_db_trade.status = TradeStatus.CLOSED
			mock_crud.getTrade.return_value = mock_db_trade
			
			mock_trading_hub_instance = AsyncMock()
			mock_trading_hub.return_value = mock_trading_hub_instance
			mock_trading_hub_instance.send_notification = AsyncMock()
			
			# Mock scheduler with tracking job
			mock_scheduler_instance = Mock()
			mock_tracking_job = Mock()
			mock_scheduler_instance.get_job = Mock(return_value=mock_tracking_job)
			mock_scheduler_instance.remove_job = Mock()
			mock_scheduler.return_value = mock_scheduler_instance
			
			# Create TradeManager instance
			tm = TradeManager()
			tm._trades = [managed_trade]
			
			# Mock _close_trade and _emit_trade_exit_event_delayed to track calls
			tm._close_trade = Mock()
			tm._emit_trade_exit_event_delayed = AsyncMock()
			
			# Simulate FILLED event from broker (after execution details already closed trade)
			# Note: closing_order.status is still OPEN here - the status change happens inside _onOrderStatusChanged
			closing_order.averageFillPrice = -0.9
			
			# Run order status change
			asyncio.run(tm._onOrderStatusChanged(closing_order, OrderStatus.FILLED))
			
			# Verify trade is still closed
			self.assertEqual(managed_trade.status, TradeStatus.CLOSED)
			
			# Verify closing order status was updated to FILLED
			self.assertEqual(closing_order.status, OrderStatus.FILLED)
			
			# Verify _close_trade() was NOT called (trade already closed)
			tm._close_trade.assert_not_called()
			
			# Verify debug log about skipping _close_trade
			debug_calls = [str(call) for call in mock_logger.debug.call_args_list]
			self.assertTrue(
				any("already closed by execution details" in call and "skipping _close_trade()" in call 
				    for call in debug_calls),
				"Expected debug log about skipping _close_trade()"
			)
			
			# Verify flow event WAS emitted (we still want events/notifications)
			self.assertEqual(tm._emit_trade_exit_event_delayed.call_count, 1)
			
			# Verify success log was written
			mock_logger.success.assert_called_once()
			self.assertIn("finished", str(mock_logger.success.call_args))
			
			# Verify notification was sent
			mock_trading_hub_instance.send_notification.assert_called_once()
			
			# Verify tracking job was removed
			mock_scheduler_instance.get_job.assert_called_once_with('TrackClosingOrder691')
			mock_scheduler_instance.remove_job.assert_called_once_with('TrackClosingOrder691')
	
	def test_duplicate_filled_event_take_profit_ignored(self):
		"""
		OTB-XXX: Test that FILLED event after execution details is handled correctly for Take Profit.
		
		Similar to closing order scenario, but for Take Profit exits.
		Execution details close the trade first, then status event arrives.
		"""
		# Create mock managed trade
		mock_trade = Mock(spec=models.Trade)
		mock_trade.id = 789
		mock_trade.status = TradeStatus.OPEN
		
		mock_template = Mock(spec=Template)
		mock_template.name = "TestTemplate"
		mock_template.account = "DU6504444"
		
		# Create mock entry order
		entry_order = Mock(spec=Order)
		entry_order.broker_order_id = '130599'
		entry_order.status = OrderStatus.FILLED
		entry_order.averageFillPrice = 0.60
		
		managed_trade = ManagedTrade(mock_trade, mock_template, entry_order, "DU6504444")
		# Simulate that execution details event already closed the trade
		managed_trade.status = TradeStatus.CLOSED
		
		# Create mock take profit order
		tp_order = Mock(spec=Order)
		tp_order.broker_order_id = '130600'
		tp_order.status = OrderStatus.OPEN
		tp_order.averageFillPrice = -1.20
		tp_order.symbol = 'SPX'
		managed_trade.takeProfitOrder = tp_order
		
		with patch('optrabot.trademanager.Session') as mock_session, \
		     patch('optrabot.trademanager.get_db_engine') as mock_engine, \
		     patch('optrabot.trademanager.crud') as mock_crud, \
		     patch('optrabot.trademanager.BrokerFactory') as mock_broker_factory, \
		     patch('optrabot.tradinghubclient.TradinghubClient') as mock_trading_hub, \
		     patch('optrabot.trademanager.AsyncIOScheduler') as mock_scheduler, \
		     patch('optrabot.trademanager.logger') as mock_logger:
			
			# Setup mocks
			mock_session_instance = MagicMock()
			mock_session.return_value.__enter__.return_value = mock_session_instance
			
			mock_db_trade = Mock(spec=models.Trade)
			mock_db_trade.id = 789
			mock_db_trade.status = TradeStatus.CLOSED
			mock_crud.getTrade.return_value = mock_db_trade
			
			mock_trading_hub_instance = AsyncMock()
			mock_trading_hub.return_value = mock_trading_hub_instance
			mock_trading_hub_instance.send_notification = AsyncMock()
			
			# Mock scheduler
			mock_scheduler_instance = Mock()
			mock_scheduler.return_value = mock_scheduler_instance
			
			# Create TradeManager instance
			tm = TradeManager()
			tm._trades = [managed_trade]
			tm._close_trade = Mock()
			tm._emit_trade_exit_event_delayed = AsyncMock()
			
			# Simulate FILLED event (after execution details already closed trade)
			# Note: tp_order.status is still OPEN here - the status change happens inside _onOrderStatusChanged
			tp_order.averageFillPrice = -1.20
			asyncio.run(tm._onOrderStatusChanged(tp_order, OrderStatus.FILLED))
			
			# Verify _close_trade() was NOT called
			tm._close_trade.assert_not_called()
			
			# Verify flow event WAS emitted
			self.assertEqual(tm._emit_trade_exit_event_delayed.call_count, 1)
			
			# Verify notification was sent
			mock_trading_hub_instance.send_notification.assert_called_once()
			
			# Verify debug log about skipping _close_trade
			debug_calls = [str(call) for call in mock_logger.debug.call_args_list]
			self.assertTrue(
				any("already closed by execution details" in call 
				    for call in debug_calls)
			)
	
	def test_duplicate_filled_event_stop_loss_ignored(self):
		"""
		OTB-XXX: Test that FILLED event after execution details is handled correctly for Stop Loss.
		
		Similar to closing order scenario, but for Stop Loss exits.
		Execution details close the trade first, then status event arrives.
		"""
		# Create mock managed trade
		mock_trade = Mock(spec=models.Trade)
		mock_trade.id = 790
		mock_trade.status = TradeStatus.OPEN
		
		mock_template = Mock(spec=Template)
		mock_template.name = "TestTemplate"
		mock_template.account = "DU6504444"
		
		# Create mock entry order
		entry_order = Mock(spec=Order)
		entry_order.broker_order_id = '130699'
		entry_order.status = OrderStatus.FILLED
		entry_order.averageFillPrice = 0.60
		
		managed_trade = ManagedTrade(mock_trade, mock_template, entry_order, "DU6504444")
		# Simulate that execution details event already closed the trade
		managed_trade.status = TradeStatus.CLOSED
		
		# Create mock stop loss order
		sl_order = Mock(spec=Order)
		sl_order.broker_order_id = '130700'
		sl_order.status = OrderStatus.OPEN
		sl_order.averageFillPrice = 1.50
		sl_order.symbol = 'SPX'
		managed_trade.stopLossOrder = sl_order
		
		with patch('optrabot.trademanager.Session') as mock_session, \
		     patch('optrabot.trademanager.get_db_engine') as mock_engine, \
		     patch('optrabot.trademanager.crud') as mock_crud, \
		     patch('optrabot.trademanager.BrokerFactory') as mock_broker_factory, \
		     patch('optrabot.tradinghubclient.TradinghubClient') as mock_trading_hub, \
		     patch('optrabot.trademanager.AsyncIOScheduler') as mock_scheduler, \
		     patch('optrabot.trademanager.logger') as mock_logger:
			
			# Setup mocks
			mock_session_instance = MagicMock()
			mock_session.return_value.__enter__.return_value = mock_session_instance
			
			mock_db_trade = Mock(spec=models.Trade)
			mock_db_trade.id = 790
			mock_db_trade.status = TradeStatus.CLOSED
			mock_crud.getTrade.return_value = mock_db_trade
			
			mock_trading_hub_instance = AsyncMock()
			mock_trading_hub.return_value = mock_trading_hub_instance
			mock_trading_hub_instance.send_notification = AsyncMock()
			
			# Mock scheduler
			mock_scheduler_instance = Mock()
			mock_scheduler.return_value = mock_scheduler_instance
			
			# Create TradeManager instance
			tm = TradeManager()
			tm._trades = [managed_trade]
			tm._close_trade = Mock()
			tm._emit_trade_exit_event_delayed = AsyncMock()
			
			# Simulate FILLED event (after execution details already closed trade)
			# Note: sl_order.status is still OPEN here - the status change happens inside _onOrderStatusChanged
			sl_order.averageFillPrice = 1.50
			asyncio.run(tm._onOrderStatusChanged(sl_order, OrderStatus.FILLED))
			
			# Verify _close_trade() was NOT called
			tm._close_trade.assert_not_called()
			
			# Verify flow event WAS emitted
			self.assertEqual(tm._emit_trade_exit_event_delayed.call_count, 1)
			
			# Verify notification was sent
			mock_trading_hub_instance.send_notification.assert_called_once()
			
			# Verify debug log about skipping _close_trade
			debug_calls = [str(call) for call in mock_logger.debug.call_args_list]
			self.assertTrue(
				any("already closed by execution details" in call 
				    for call in debug_calls)
			)


if __name__ == '__main__':
	unittest.main()
