class CCXTDataClientTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = LiveClock()
        self.uuid_factory = UUIDFactory()
        self.trader_id = TraderId("TESTER", "001")

        # Fresh isolated loop testing pattern
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

        # Setup logging
        logger = LiveLogger(
            clock=self.clock,
            name=self.trader_id.value,
            level_console=LogLevel.INFO,
            level_file=LogLevel.DEBUG,
            level_store=LogLevel.WARNING,
        )

        self.logger = LiveLogger(self.clock)

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )

        self.data_engine = LiveDataEngine(
            loop=self.loop,
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

        # Setup mock CCXT exchange
        with open(TEST_PATH + "markets.json") as response:
            markets = json.load(response)

        with open(TEST_PATH + "currencies.json") as response:
            currencies = json.load(response)

        with open(TEST_PATH + "watch_order_book.json") as response:
            order_book = json.load(response)

        with open(TEST_PATH + "fetch_trades.json") as response:
            fetch_trades = json.load(response)

        with open(TEST_PATH + "watch_trades.json") as response:
            watch_trades = json.load(response)

        self.mock_ccxt = MagicMock()
        self.mock_ccxt.name = "Binance"
        self.mock_ccxt.precisionMode = 2
        self.mock_ccxt.markets = markets
        self.mock_ccxt.currencies = currencies
        self.mock_ccxt.watch_order_book = order_book
        self.mock_ccxt.watch_trades = watch_trades
        self.mock_ccxt.fetch_trades = fetch_trades

        self.client = CCXTDataClient(
            client=self.mock_ccxt,
            engine=self.data_engine,
            clock=self.clock,
            logger=logger,
        )

        self.data_engine.register_client(self.client)

    def tearDown(self):
        self.loop.stop()
        self.loop.close()

    def test_connect(self):
        async def run_test():
            # Arrange
            # Act
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Assert
            self.assertTrue(self.client.is_connected)

            # Tear down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_disconnect(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.disconnect()
            await asyncio.sleep(0.3)

            # Assert
            self.assertFalse(self.client.is_connected)

            # Tear down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_reset_when_not_connected_successfully_resets(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            self.data_engine.stop()
            await asyncio.sleep(0.3)  # Allow engine message queue to stop

            # Act
            self.client.reset()

            # Assert
            self.assertFalse(self.client.is_connected)

        self.loop.run_until_complete(run_test())

    def test_reset_when_connected_does_not_reset(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.reset()

            # Assert
            self.assertTrue(self.client.is_connected)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_dispose_when_not_connected_does_not_dispose(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.dispose()

            # Assert
            self.assertTrue(self.client.is_connected)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_subscribe_instrument(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.subscribe_instrument(BTCUSDT)

            # Assert
            self.assertIn(BTCUSDT, self.client.subscribed_instruments)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_subscribe_quote_ticks(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.subscribe_quote_ticks(ETHUSDT)
            await asyncio.sleep(0.3)

            # Assert
            self.assertIn(ETHUSDT, self.client.subscribed_quote_ticks)
            self.assertTrue(self.data_engine.cache.has_quote_ticks(ETHUSDT))

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_subscribe_trade_ticks(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.subscribe_trade_ticks(ETHUSDT)
            await asyncio.sleep(0.3)

            # Assert
            self.assertIn(ETHUSDT, self.client.subscribed_trade_ticks)
            self.assertTrue(self.data_engine.cache.has_trade_ticks(ETHUSDT))

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_subscribe_bars(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.5)  # Allow engine message queue to start

            bar_type = TestStubs.bartype_btcusdt_binance_100tick_last()

            # Act
            self.client.subscribe_bars(bar_type)

            # Assert
            self.assertIn(bar_type, self.client.subscribed_bars)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_unsubscribe_instrument(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            self.client.subscribe_instrument(BTCUSDT)

            # Act
            self.client.unsubscribe_instrument(BTCUSDT)

            # Assert
            self.assertNotIn(BTCUSDT, self.client.subscribed_instruments)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_unsubscribe_quote_ticks(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            self.client.subscribe_quote_ticks(ETHUSDT)
            await asyncio.sleep(0.3)

            # Act
            self.client.unsubscribe_quote_ticks(ETHUSDT)

            # Assert
            self.assertNotIn(ETHUSDT, self.client.subscribed_quote_ticks)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_unsubscribe_trade_ticks(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            self.client.subscribe_trade_ticks(ETHUSDT)

            # Act
            self.client.unsubscribe_trade_ticks(ETHUSDT)

            # Assert
            self.assertNotIn(ETHUSDT, self.client.subscribed_trade_ticks)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_unsubscribe_bars(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            bar_type = TestStubs.bartype_btcusdt_binance_100tick_last()
            self.client.subscribe_bars(bar_type)

            # Act
            self.client.unsubscribe_bars(bar_type)

            # Assert
            self.assertNotIn(bar_type, self.client.subscribed_bars)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_instrument(self):
        async def run_test():
            # Arrange
            self.data_engine.start()
            await asyncio.sleep(0.5)  # Allow engine message queue to start

            # Act
            self.client.request_instrument(BTCUSDT, uuid4())
            await asyncio.sleep(0.5)

            # Assert
            # Instruments additionally requested on start
            self.assertEqual(1, self.data_engine.response_count)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_instruments(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.5)  # Allow engine message queue to start

            # Act
            self.client.request_instruments(uuid4())
            await asyncio.sleep(0.5)

            # Assert
            # Instruments additionally requested on start
            self.assertEqual(1, self.data_engine.response_count)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_quote_ticks(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            # Act
            self.client.request_quote_ticks(BTCUSDT, None, None, 0, uuid4())

            # Assert
            self.assertTrue(True)  # Logs warning

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_trade_ticks(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            handler = ObjectStorer()

            request = DataRequest(
                venue=BINANCE,
                data_type=TradeTick,
                metadata={
                    "Symbol": ETHUSDT,
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 100,
                },
                callback=handler.store,
                request_id=self.uuid_factory.generate(),
                request_timestamp=self.clock.utc_now(),
            )

            # Act
            self.data_engine.send(request)

            await asyncio.sleep(1)

            # Assert
            self.assertEqual(1, self.data_engine.response_count)
            self.assertEqual(1, handler.count)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_bars(self):
        async def run_test():
            # Arrange
            with open(TEST_PATH + "fetch_ohlcv.json") as response:
                fetch_ohlcv = json.load(response)

            self.mock_ccxt.fetch_ohlcv = fetch_ohlcv

            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)  # Allow engine message queue to start

            handler = ObjectStorer()

            bar_spec = BarSpecification(1, BarAggregation.MINUTE,
                                        PriceType.LAST)
            bar_type = BarType(symbol=ETHUSDT, bar_spec=bar_spec)

            request = DataRequest(
                venue=BINANCE,
                data_type=Bar,
                metadata={
                    "BarType": bar_type,
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 100,
                },
                callback=handler.store_2,
                request_id=self.uuid_factory.generate(),
                request_timestamp=self.clock.utc_now(),
            )

            # Act
            self.data_engine.send(request)

            await asyncio.sleep(0.3)

            # Assert
            self.assertEqual(1, self.data_engine.response_count)
            self.assertEqual(1, handler.count)
            self.assertEqual(100, len(handler.get_store()[0][1]))

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())
class LiveDataEngineTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = LiveClock()
        self.uuid_factory = UUIDFactory()
        self.logger = TestLogger(self.clock, level_console=LogLevel.DEBUG)

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )

        # Fresh isolated loop testing pattern
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

        self.data_engine = LiveDataEngine(
            loop=self.loop,
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

    def tearDown(self):
        self.data_engine.dispose()
        self.loop.stop()
        self.loop.close()

    def test_get_event_loop_returns_expected_loop(self):
        # Arrange
        # Act
        loop = self.data_engine.get_event_loop()

        # Assert
        self.assertEqual(self.loop, loop)

    def test_start(self):
        async def run_test():
            # Arrange
            # Act
            self.data_engine.start()
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(ComponentState.RUNNING, self.data_engine.state)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_execute_command_processes_message(self):
        async def run_test():
            # Arrange
            self.data_engine.start()

            subscribe = Subscribe(
                venue=BINANCE,
                data_type=QuoteTick,
                metadata={},
                handler=[].append,
                command_id=self.uuid_factory.generate(),
                command_timestamp=self.clock.utc_now(),
            )

            # Act
            self.data_engine.execute(subscribe)
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(0, self.data_engine.message_qsize())
            self.assertEqual(1, self.data_engine.command_count)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_send_request_processes_message(self):
        async def run_test():
            # Arrange
            self.data_engine.start()

            handler = []
            request = DataRequest(
                venue=Venue("RANDOM"),
                data_type=QuoteTick,
                metadata={
                    "Symbol": Symbol("SOMETHING", Venue("RANDOM")),
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 1000,
                },
                callback=handler.append,
                request_id=self.uuid_factory.generate(),
                request_timestamp=self.clock.utc_now(),
            )

            # Act
            self.data_engine.send(request)
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(0, self.data_engine.message_qsize())
            self.assertEqual(1, self.data_engine.request_count)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_receive_response_processes_message(self):
        async def run_test():
            # Arrange
            self.data_engine.start()

            response = DataResponse(
                venue=Venue("BINANCE"),
                data_type=QuoteTick,
                metadata={},
                data=[],
                correlation_id=self.uuid_factory.generate(),
                response_id=self.uuid_factory.generate(),
                response_timestamp=self.clock.utc_now(),
            )

            # Act
            self.data_engine.receive(response)
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(0, self.data_engine.message_qsize())
            self.assertEqual(1, self.data_engine.response_count)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_process_data_processes_data(self):
        async def run_test():
            # Arrange
            self.data_engine.start()

            # Act
            tick = TestStubs.trade_tick_5decimal()

            # Act
            self.data_engine.process(tick)
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(0, self.data_engine.data_qsize())
            self.assertEqual(1, self.data_engine.data_count)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())
class OandaDataClientTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = LiveClock()
        self.uuid_factory = UUIDFactory()
        self.trader_id = TraderId("TESTER", "001")

        # Fresh isolated loop testing pattern
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.executor = concurrent.futures.ThreadPoolExecutor()
        self.loop.set_default_executor(self.executor)
        self.loop.set_debug(False)  # TODO: Development

        # Setup logging
        logger = LiveLogger(
            clock=self.clock,
            name=self.trader_id.value,
            level_console=LogLevel.DEBUG,
            level_file=LogLevel.DEBUG,
            level_store=LogLevel.WARNING,
        )

        self.logger = LiveLogger(self.clock)

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )

        self.data_engine = LiveDataEngine(
            loop=self.loop,
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

        self.mock_oanda = MagicMock()

        self.client = OandaDataClient(
            client=self.mock_oanda,
            account_id="001",
            engine=self.data_engine,
            clock=self.clock,
            logger=logger,
        )

        self.data_engine.register_client(self.client)

        with open(TEST_PATH + "instruments.json") as response:
            instruments = json.load(response)

        self.mock_oanda.request.return_value = instruments

    def tearDown(self):
        self.executor.shutdown(wait=True)
        self.loop.stop()
        self.loop.close()

    def test_connect(self):
        async def run_test():
            # Arrange
            # Act
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.3)

            # Assert
            self.assertTrue(self.client.is_connected)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_disconnect(self):
        # Arrange
        self.client.connect()

        # Act
        self.client.disconnect()

        # Assert
        self.assertFalse(self.client.is_connected)

    def test_reset(self):
        # Arrange
        # Act
        self.client.reset()

        # Assert
        self.assertFalse(self.client.is_connected)

    def test_dispose(self):
        # Arrange
        # Act
        self.client.dispose()

        # Assert
        self.assertFalse(self.client.is_connected)

    def test_subscribe_instrument(self):
        # Arrange
        self.client.connect()

        # Act
        self.client.subscribe_instrument(AUDUSD)

        # Assert
        self.assertIn(AUDUSD, self.client.subscribed_instruments)

    def test_subscribe_quote_ticks(self):
        async def run_test():
            # Arrange
            self.mock_oanda.request.return_value = {"type": {"HEARTBEAT": "0"}}
            self.data_engine.start()

            # Act
            self.client.subscribe_quote_ticks(AUDUSD)
            await asyncio.sleep(0.3)

            # Assert
            self.assertIn(AUDUSD, self.client.subscribed_quote_ticks)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_subscribe_bars(self):
        # Arrange
        bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID)
        bar_type = BarType(symbol=AUDUSD, bar_spec=bar_spec)

        # Act
        self.client.subscribe_bars(bar_type)

        # Assert
        self.assertTrue(True)

    def test_unsubscribe_instrument(self):
        # Arrange
        self.client.connect()

        # Act
        self.client.unsubscribe_instrument(AUDUSD)

        # Assert
        self.assertTrue(True)

    def test_unsubscribe_quote_ticks(self):
        async def run_test():
            # Arrange
            self.mock_oanda.request.return_value = {"type": {"HEARTBEAT": "0"}}
            self.data_engine.start()

            self.client.subscribe_quote_ticks(AUDUSD)
            await asyncio.sleep(0.3)

            # # Act
            self.client.unsubscribe_quote_ticks(AUDUSD)
            await asyncio.sleep(0.3)

            # Assert
            self.assertNotIn(AUDUSD, self.client.subscribed_quote_ticks)

            # Tear Down
            self.data_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_unsubscribe_bars(self):
        # Arrange
        bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID)
        bar_type = BarType(symbol=AUDUSD, bar_spec=bar_spec)

        # Act
        self.client.unsubscribe_bars(bar_type)

        # Assert
        self.assertTrue(True)

    def test_request_instrument(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.5)

            # Act
            self.client.request_instrument(AUDUSD, uuid4())
            await asyncio.sleep(0.5)

            # Assert
            # Instruments additionally requested on start
            self.assertEqual(1, self.data_engine.response_count)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_instruments(self):
        async def run_test():
            # Arrange
            self.data_engine.start()  # Also starts client
            await asyncio.sleep(0.5)

            # Act
            self.client.request_instruments(uuid4())
            await asyncio.sleep(0.5)

            # Assert
            # Instruments additionally requested on start
            self.assertEqual(1, self.data_engine.response_count)

            # Tear Down
            self.data_engine.stop()
            await self.data_engine.get_run_queue_task()

        self.loop.run_until_complete(run_test())

    def test_request_bars(self):
        async def run_test():
            # Arrange

            with open(TEST_PATH + "instruments.json") as response:
                instruments = json.load(response)

            # Arrange
            with open(TEST_PATH + "bars.json") as response:
                bars = json.load(response)

            self.mock_oanda.request.side_effect = [instruments, bars]

            handler = ObjectStorer()
            self.data_engine.start()
            await asyncio.sleep(0.3)

            bar_spec = BarSpecification(1, BarAggregation.MINUTE,
                                        PriceType.MID)
            bar_type = BarType(symbol=AUDUSD, bar_spec=bar_spec)

            request = DataRequest(
                venue=OANDA,
                data_type=Bar,
                metadata={
                    "BarType": bar_type,
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 1000,
                },
                callback=handler.store_2,
                request_id=self.uuid_factory.generate(),
                request_timestamp=self.clock.utc_now(),
            )

            # Act
            self.data_engine.send(request)

            # Allow time for request to be sent, processed and response returned
            await asyncio.sleep(0.3)

            # Assert
            self.assertEqual(1, self.data_engine.response_count)
            self.assertEqual(1, handler.count)
            # Final bar incomplete so becomes partial
            self.assertEqual(99, len(handler.get_store()[0][1]))

            # Tear Down
            self.data_engine.stop()
            self.data_engine.dispose()

        self.loop.run_until_complete(run_test())