"""
Binance Spot WebSocket Streams

OpenAPI Specifications for the Binance Spot WebSocket Streams

API documents:
  - [Github web-socket-streams documentation file](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md)
  - [General API information for web-socket-streams on website](https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams)

The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)

Do not edit the class manually.
"""

import pytest

from unittest.mock import AsyncMock, MagicMock

from binance_common.configuration import ConfigurationWebSocketStreams
from binance_common.errors import RequiredError
from binance_common.utils import ws_streams_placeholder
from binance_common.websocket import WebSocketStreamBase, RequestStreamHandle
from binance_sdk_spot.websocket_streams.streams import WebSocketStreamsApi


from binance_sdk_spot.websocket_streams.models import (
    AllMarketRollingWindowTickerWindowSizeEnum,
)
from binance_sdk_spot.websocket_streams.models import KlineIntervalEnum
from binance_sdk_spot.websocket_streams.models import KlineOffsetIntervalEnum
from binance_sdk_spot.websocket_streams.models import PartialBookDepthLevelsEnum
from binance_sdk_spot.websocket_streams.models import RollingWindowTickerWindowSizeEnum


@pytest.fixture
def mock_websocket_client():
    """Fixture to provide a reusable mocked WebSocketStreamBase instance."""
    config = ConfigurationWebSocketStreams(stream_url="wss://mock-url")
    websocket_stream_client = WebSocketStreamBase(config)
    websocket_stream_client.subscribe = AsyncMock()
    websocket_stream_client.unsubscribe = AsyncMock()
    return websocket_stream_client


class TestWebSocketStreams:
    @pytest.fixture(autouse=True)
    def setup(self, mock_websocket_client):
        self.websocket_client = mock_websocket_client
        self.ws_streams = WebSocketStreamsApi(websocket_base=self.websocket_client)

    @pytest.mark.asyncio
    async def test_agg_trade_subscription(self):
        """Test that agg_trade() subscribes to the correct WebSocket stream."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "e": "aggTrade",
            "E": 1672515782136,
            "s": "BNBBTC",
            "a": 12345,
            "p": "0.001",
            "q": "100",
            "f": 100,
            "l": 105,
            "T": 1672515782136,
            "m": True,
            "M": True,
        }
        stream_endpoint = ws_streams_placeholder(
            "/<symbol>@aggTrade".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.agg_trade(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_agg_trade_success(self):
        """Test agg_trade() successfully with required parameters only."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "e": "aggTrade",
            "E": 1672515782136,
            "s": "BNBBTC",
            "a": 12345,
            "p": "0.001",
            "q": "100",
            "f": 100,
            "l": 105,
            "T": 1672515782136,
            "m": True,
            "M": True,
        }
        self.ws_streams.agg_trade = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.agg_trade(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_agg_trade_success_with_optional_params(self):
        """Test agg_trade() successfully with optional parameters."""

        params = {"symbol": "bnbusdt", "id": "e9d6b4349871b40611412680b3445fac"}

        expected_response = {
            "e": "aggTrade",
            "E": 1672515782136,
            "s": "BNBBTC",
            "a": 12345,
            "p": "0.001",
            "q": "100",
            "f": 100,
            "l": 105,
            "T": 1672515782136,
            "m": True,
            "M": True,
        }

        self.ws_streams.agg_trade = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.agg_trade(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_agg_trade_missing_required_param_symbol(self):
        """Test that agg_trade() raises RequiredError when 'symbol' is missing."""
        params = {
            "symbol": "bnbusdt",
        }
        params["symbol"] = None

        with pytest.raises(RequiredError, match="Missing required parameter 'symbol'"):
            await self.ws_streams.agg_trade(**params)

    @pytest.mark.asyncio
    async def test_agg_trade_server_error(self):
        """Test that agg_trade() raises an error when the server returns an error."""

        params = {
            "symbol": "bnbusdt",
        }

        mock_error = Exception("ResponseError")
        self.ws_streams.agg_trade = MagicMock(side_effect=mock_error)

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.agg_trade(**params)

    @pytest.mark.asyncio
    async def test_all_market_rolling_window_ticker_subscription(self):
        """Test that all_market_rolling_window_ticker() subscribes to the correct WebSocket stream."""

        params = {
            "window_size": AllMarketRollingWindowTickerWindowSizeEnum[
                "WINDOW_SIZE_1h"
            ].value,
        }

        expected_response = [
            {
                "e": "1hTicker",
                "E": 1672515782136,
                "s": "BNBBTC",
                "p": "0.0015",
                "P": "250.00",
                "o": "0.0010",
                "h": "0.0025",
                "l": "0.0010",
                "c": "0.0025",
                "w": "0.0018",
                "v": "10000",
                "q": "18",
                "O": 0,
                "C": 1675216573749,
                "F": 0,
                "L": 18150,
                "n": 18151,
            }
        ]
        stream_endpoint = ws_streams_placeholder(
            "/!ticker_<windowSize>@arr".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.all_market_rolling_window_ticker(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_all_market_rolling_window_ticker_success(self):
        """Test all_market_rolling_window_ticker() successfully with required parameters only."""

        params = {
            "window_size": AllMarketRollingWindowTickerWindowSizeEnum[
                "WINDOW_SIZE_1h"
            ].value,
        }

        expected_response = [
            {
                "e": "1hTicker",
                "E": 1672515782136,
                "s": "BNBBTC",
                "p": "0.0015",
                "P": "250.00",
                "o": "0.0010",
                "h": "0.0025",
                "l": "0.0010",
                "c": "0.0025",
                "w": "0.0018",
                "v": "10000",
                "q": "18",
                "O": 0,
                "C": 1675216573749,
                "F": 0,
                "L": 18150,
                "n": 18151,
            }
        ]
        self.ws_streams.all_market_rolling_window_ticker = AsyncMock(
            return_value=expected_response
        )

        response = await self.ws_streams.all_market_rolling_window_ticker(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_all_market_rolling_window_ticker_success_with_optional_params(self):
        """Test all_market_rolling_window_ticker() successfully with optional parameters."""

        params = {
            "window_size": AllMarketRollingWindowTickerWindowSizeEnum[
                "WINDOW_SIZE_1h"
            ].value,
            "id": "e9d6b4349871b40611412680b3445fac",
        }

        expected_response = [
            {
                "e": "1hTicker",
                "E": 1672515782136,
                "s": "BNBBTC",
                "p": "0.0015",
                "P": "250.00",
                "o": "0.0010",
                "h": "0.0025",
                "l": "0.0010",
                "c": "0.0025",
                "w": "0.0018",
                "v": "10000",
                "q": "18",
                "O": 0,
                "C": 1675216573749,
                "F": 0,
                "L": 18150,
                "n": 18151,
            }
        ]

        self.ws_streams.all_market_rolling_window_ticker = AsyncMock(
            return_value=expected_response
        )

        response = await self.ws_streams.all_market_rolling_window_ticker(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_all_market_rolling_window_ticker_missing_required_param_window_size(
        self,
    ):
        """Test that all_market_rolling_window_ticker() raises RequiredError when 'window_size' is missing."""
        params = {
            "window_size": AllMarketRollingWindowTickerWindowSizeEnum[
                "WINDOW_SIZE_1h"
            ].value,
        }
        params["window_size"] = None

        with pytest.raises(
            RequiredError, match="Missing required parameter 'window_size'"
        ):
            await self.ws_streams.all_market_rolling_window_ticker(**params)

    @pytest.mark.asyncio
    async def test_all_market_rolling_window_ticker_server_error(self):
        """Test that all_market_rolling_window_ticker() raises an error when the server returns an error."""

        params = {
            "window_size": AllMarketRollingWindowTickerWindowSizeEnum[
                "WINDOW_SIZE_1h"
            ].value,
        }

        mock_error = Exception("ResponseError")
        self.ws_streams.all_market_rolling_window_ticker = MagicMock(
            side_effect=mock_error
        )

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.all_market_rolling_window_ticker(**params)

    @pytest.mark.asyncio
    async def test_all_mini_ticker_subscription(self):
        """Test that all_mini_ticker() subscribes to the correct WebSocket stream."""

        expected_response = [
            {
                "e": "24hrMiniTicker",
                "E": 1672515782136,
                "s": "BNBBTC",
                "c": "0.0025",
                "o": "0.0010",
                "h": "0.0025",
                "l": "0.0010",
                "v": "10000",
                "q": "18",
            }
        ]
        stream_endpoint = ws_streams_placeholder(
            "/!miniTicker@arr".replace("/", "", 1),
            {},
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.all_mini_ticker()
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_all_mini_ticker_success(self):
        """Test all_mini_ticker() successfully with required parameters only."""

        expected_response = [
            {
                "e": "24hrMiniTicker",
                "E": 1672515782136,
                "s": "BNBBTC",
                "c": "0.0025",
                "o": "0.0010",
                "h": "0.0025",
                "l": "0.0010",
                "v": "10000",
                "q": "18",
            }
        ]
        self.ws_streams.all_mini_ticker = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.all_mini_ticker()
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_all_mini_ticker_success_with_optional_params(self):
        """Test all_mini_ticker() successfully with optional parameters."""

        params = {"id": "e9d6b4349871b40611412680b3445fac"}

        expected_response = [
            {
                "e": "24hrMiniTicker",
                "E": 1672515782136,
                "s": "BNBBTC",
                "c": "0.0025",
                "o": "0.0010",
                "h": "0.0025",
                "l": "0.0010",
                "v": "10000",
                "q": "18",
            }
        ]

        self.ws_streams.all_mini_ticker = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.all_mini_ticker(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_all_mini_ticker_server_error(self):
        """Test that all_mini_ticker() raises an error when the server returns an error."""

        mock_error = Exception("ResponseError")
        self.ws_streams.all_mini_ticker = MagicMock(side_effect=mock_error)

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.all_mini_ticker()

    @pytest.mark.asyncio
    async def test_avg_price_subscription(self):
        """Test that avg_price() subscribes to the correct WebSocket stream."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "e": "avgPrice",
            "E": 1693907033000,
            "s": "BTCUSDT",
            "i": "5m",
            "w": "25776.86000000",
            "T": 1693907032213,
        }
        stream_endpoint = ws_streams_placeholder(
            "/<symbol>@avgPrice".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.avg_price(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_avg_price_success(self):
        """Test avg_price() successfully with required parameters only."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "e": "avgPrice",
            "E": 1693907033000,
            "s": "BTCUSDT",
            "i": "5m",
            "w": "25776.86000000",
            "T": 1693907032213,
        }
        self.ws_streams.avg_price = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.avg_price(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_avg_price_success_with_optional_params(self):
        """Test avg_price() successfully with optional parameters."""

        params = {"symbol": "bnbusdt", "id": "e9d6b4349871b40611412680b3445fac"}

        expected_response = {
            "e": "avgPrice",
            "E": 1693907033000,
            "s": "BTCUSDT",
            "i": "5m",
            "w": "25776.86000000",
            "T": 1693907032213,
        }

        self.ws_streams.avg_price = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.avg_price(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_avg_price_missing_required_param_symbol(self):
        """Test that avg_price() raises RequiredError when 'symbol' is missing."""
        params = {
            "symbol": "bnbusdt",
        }
        params["symbol"] = None

        with pytest.raises(RequiredError, match="Missing required parameter 'symbol'"):
            await self.ws_streams.avg_price(**params)

    @pytest.mark.asyncio
    async def test_avg_price_server_error(self):
        """Test that avg_price() raises an error when the server returns an error."""

        params = {
            "symbol": "bnbusdt",
        }

        mock_error = Exception("ResponseError")
        self.ws_streams.avg_price = MagicMock(side_effect=mock_error)

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.avg_price(**params)

    @pytest.mark.asyncio
    async def test_book_ticker_subscription(self):
        """Test that book_ticker() subscribes to the correct WebSocket stream."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "u": 400900217,
            "s": "BNBUSDT",
            "b": "25.35190000",
            "B": "31.21000000",
            "a": "25.36520000",
            "A": "40.66000000",
        }
        stream_endpoint = ws_streams_placeholder(
            "/<symbol>@bookTicker".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.book_ticker(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_book_ticker_success(self):
        """Test book_ticker() successfully with required parameters only."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "u": 400900217,
            "s": "BNBUSDT",
            "b": "25.35190000",
            "B": "31.21000000",
            "a": "25.36520000",
            "A": "40.66000000",
        }
        self.ws_streams.book_ticker = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.book_ticker(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_book_ticker_success_with_optional_params(self):
        """Test book_ticker() successfully with optional parameters."""

        params = {"symbol": "bnbusdt", "id": "e9d6b4349871b40611412680b3445fac"}

        expected_response = {
            "u": 400900217,
            "s": "BNBUSDT",
            "b": "25.35190000",
            "B": "31.21000000",
            "a": "25.36520000",
            "A": "40.66000000",
        }

        self.ws_streams.book_ticker = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.book_ticker(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_book_ticker_missing_required_param_symbol(self):
        """Test that book_ticker() raises RequiredError when 'symbol' is missing."""
        params = {
            "symbol": "bnbusdt",
        }
        params["symbol"] = None

        with pytest.raises(RequiredError, match="Missing required parameter 'symbol'"):
            await self.ws_streams.book_ticker(**params)

    @pytest.mark.asyncio
    async def test_book_ticker_server_error(self):
        """Test that book_ticker() raises an error when the server returns an error."""

        params = {
            "symbol": "bnbusdt",
        }

        mock_error = Exception("ResponseError")
        self.ws_streams.book_ticker = MagicMock(side_effect=mock_error)

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.book_ticker(**params)

    @pytest.mark.asyncio
    async def test_diff_book_depth_subscription(self):
        """Test that diff_book_depth() subscribes to the correct WebSocket stream."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "e": "depthUpdate",
            "E": 1672515782136,
            "s": "BNBBTC",
            "U": 157,
            "u": 160,
            "b": [["0.0024", "10"]],
            "a": [["0.0026", "100"]],
        }
        stream_endpoint = ws_streams_placeholder(
            "/<symbol>@depth@<updateSpeed>".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.diff_book_depth(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_diff_book_depth_success(self):
        """Test diff_book_depth() successfully with required parameters only."""

        params = {
            "symbol": "bnbusdt",
        }

        expected_response = {
            "e": "depthUpdate",
            "E": 1672515782136,
            "s": "BNBBTC",
            "U": 157,
            "u": 160,
            "b": [["0.0024", "10"]],
            "a": [["0.0026", "100"]],
        }
        self.ws_streams.diff_book_depth = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.diff_book_depth(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_diff_book_depth_success_with_optional_params(self):
        """Test diff_book_depth() successfully with optional parameters."""

        params = {
            "symbol": "bnbusdt",
            "id": "e9d6b4349871b40611412680b3445fac",
            "update_speed": "100ms",
        }

        expected_response = {
            "e": "depthUpdate",
            "E": 1672515782136,
            "s": "BNBBTC",
            "U": 157,
            "u": 160,
            "b": [["0.0024", "10"]],
            "a": [["0.0026", "100"]],
        }

        self.ws_streams.diff_book_depth = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.diff_book_depth(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_diff_book_depth_missing_required_param_symbol(self):
        """Test that diff_book_depth() raises RequiredError when 'symbol' is missing."""
        params = {
            "symbol": "bnbusdt",
        }
        params["symbol"] = None

        with pytest.raises(RequiredError, match="Missing required parameter 'symbol'"):
            await self.ws_streams.diff_book_depth(**params)

    @pytest.mark.asyncio
    async def test_diff_book_depth_server_error(self):
        """Test that diff_book_depth() raises an error when the server returns an error."""

        params = {
            "symbol": "bnbusdt",
        }

        mock_error = Exception("ResponseError")
        self.ws_streams.diff_book_depth = MagicMock(side_effect=mock_error)

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.diff_book_depth(**params)

    @pytest.mark.asyncio
    async def test_kline_subscription(self):
        """Test that kline() subscribes to the correct WebSocket stream."""

        params = {
            "symbol": "bnbusdt",
            "interval": KlineIntervalEnum["INTERVAL_1s"].value,
        }

        expected_response = {
            "e": "kline",
            "E": 1672515782136,
            "s": "BNBBTC",
            "k": {
                "t": 1672515780000,
                "T": 1672515839999,
                "s": "BNBBTC",
                "i": "1m",
                "f": 100,
                "L": 200,
                "o": "0.0010",
                "c": "0.0020",
                "h": "0.0025",
                "l": "0.0015",
                "v": "1000",
                "n": 100,
                "x": False,
                "q": "1.0000",
                "V": "500",
                "Q": "0.500",
                "B": "123456",
            },
        }
        stream_endpoint = ws_streams_placeholder(
            "/<symbol>@kline_<interval>".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.kline(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_kline_success(self):
        """Test kline() successfully with required parameters only."""

        params = {
            "symbol": "bnbusdt",
            "interval": KlineIntervalEnum["INTERVAL_1s"].value,
        }

        expected_response = {
            "e": "kline",
            "E": 1672515782136,
            "s": "BNBBTC",
            "k": {
                "t": 1672515780000,
                "T": 1672515839999,
                "s": "BNBBTC",
                "i": "1m",
                "f": 100,
                "L": 200,
                "o": "0.0010",
                "c": "0.0020",
                "h": "0.0025",
                "l": "0.0015",
                "v": "1000",
                "n": 100,
                "x": False,
                "q": "1.0000",
                "V": "500",
                "Q": "0.500",
                "B": "123456",
            },
        }
        self.ws_streams.kline = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.kline(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_kline_success_with_optional_params(self):
        """Test kline() successfully with optional parameters."""

        params = {
            "symbol": "bnbusdt",
            "interval": KlineIntervalEnum["INTERVAL_1s"].value,
            "id": "e9d6b4349871b40611412680b3445fac",
        }

        expected_response = {
            "e": "kline",
            "E": 1672515782136,
            "s": "BNBBTC",
            "k": {
                "t": 1672515780000,
                "T": 1672515839999,
                "s": "BNBBTC",
                "i": "1m",
                "f": 100,
                "L": 200,
                "o": "0.0010",
                "c": "0.0020",
                "h": "0.0025",
                "l": "0.0015",
                "v": "1000",
                "n": 100,
                "x": False,
                "q": "1.0000",
                "V": "500",
                "Q": "0.500",
                "B": "123456",
            },
        }

        self.ws_streams.kline = AsyncMock(return_value=expected_response)

        response = await self.ws_streams.kline(**params)
        assert response is not None
        assert response == expected_response

    @pytest.mark.asyncio
    async def test_kline_missing_required_param_symbol(self):
        """Test that kline() raises RequiredError when 'symbol' is missing."""
        params = {
            "symbol": "bnbusdt",
            "interval": KlineIntervalEnum["INTERVAL_1s"].value,
        }
        params["symbol"] = None

        with pytest.raises(RequiredError, match="Missing required parameter 'symbol'"):
            await self.ws_streams.kline(**params)

    @pytest.mark.asyncio
    async def test_kline_missing_required_param_interval(self):
        """Test that kline() raises RequiredError when 'interval' is missing."""
        params = {
            "symbol": "bnbusdt",
            "interval": KlineIntervalEnum["INTERVAL_1s"].value,
        }
        params["interval"] = None

        with pytest.raises(
            RequiredError, match="Missing required parameter 'interval'"
        ):
            await self.ws_streams.kline(**params)

    @pytest.mark.asyncio
    async def test_kline_server_error(self):
        """Test that kline() raises an error when the server returns an error."""

        params = {
            "symbol": "bnbusdt",
            "interval": KlineIntervalEnum["INTERVAL_1s"].value,
        }

        mock_error = Exception("ResponseError")
        self.ws_streams.kline = MagicMock(side_effect=mock_error)

        with pytest.raises(Exception, match="ResponseError"):
            await self.ws_streams.kline(**params)

    @pytest.mark.asyncio
    async def test_kline_offset_subscription(self):
        """Test that kline_offset() subscribes to the correct WebSocket stream."""

        params = {
            "symbol": "bnbusdt",
            "interval": KlineOffsetIntervalEnum["INTERVAL_1s"].value,
        }

        expected_response = {
            "e": "kline",
            "E": 1672515782136,
            "s": "BNBBTC",
            "k": {
                "t": 1672515780000,
                "T": 1672515839999,
                "s": "BNBBTC",
                "i": "1m",
                "f": 100,
                "L": 200,
                "o": "0.0010",
                "c": "0.0020",
                "h": "0.0025",
                "l": "0.0015",
                "v": "1000",
                "n": 100,
                "x": False,
                "q": "1.0000",
                "V": "500",
                "Q": "0.500",
                "B": "123456",
            },
        }
        stream_endpoint = ws_streams_placeholder(
            "/<symbol>@kline_<interval>@+08:00".replace("/", "", 1),
            params,
        )

        def mock_on(event, callback, stream):
            if event == "message" and stream == stream_endpoint:
                callback(expected_response)

        self.websocket_client.on = MagicMock(side_effect=mock_on)

        mock_callback = MagicMock()

        stream = await self.ws_streams.kline_offset(**params)
        assert isinstance(stream, RequestStreamHandle)
        assert callable(stream.on)
        assert callable(stream.unsubscribe)
        stream.on("message", mock_callback)
        mock_callback.assert_called_once_with(expected_response)

    @pytest.mark.asyncio
    async def test_kline_offset_success(self):
        """Test kline_offset() successfully with required parameters only."""

        params = {
            "symbol": "bnbusdt",
            "interval": KlineOffsetIntervalEnum["INTERVAL_1s"].value,
        }

        expected_response = {
            "e": "kline",
            "E": 1672515782136,
            "s": "BNBBTC",
            "k": {
                "t": 1672515780000,
                "T": 1672515839999,
                "s": "BNBBTC",
                "i": "1m",
                "f": 100,
                "L": 200,
                "o": "0.0010",
                "c": "0.0020",
                "h": "0.0025",
                "l": "0.0015",
                "v": "1000",
                "n": 100,
                "x": False,
                "q": "1.0000",
                "V": "500",
                "Q": "0.500",
                "B": "123456",
            },
