"""Tests for Binance Vision UM futures aggTrades parsing (Issue #278).

Covers the 5 key differences between spot and UM futures Vision CSVs:
1. 7 columns (no was_best_price) vs spot's 8
2. Header row from 2022-08-11 onward (spot: never)
3. Timestamps always milliseconds (spot: switched to us on 2025-01-01)
4. Lowercase booleans (true/false) vs spot's Title case (True/False)
5. Variable decimal precision in price/quantity
"""

from __future__ import annotations

import io

# ---------------------------------------------------------------------------
# Import seeder internals
# ---------------------------------------------------------------------------
import sys
import zipfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))

from tick_cache_seeder import (
    _UM_COLUMNS,
    _UM_HEADER_CUTOFF,
    _cache_key,
    _download_vision_day,
    _parse_markets,
    _vision_url,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_vision_zip(symbol: str, date_str: str, csv_content: str) -> bytes:
    """Create an in-memory ZIP file mimicking Binance Vision format."""
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
        csv_name = f"{symbol}-aggTrades-{date_str}.csv"
        zf.writestr(csv_name, csv_content)
    return buf.getvalue()


def _mock_response(status_code: int, content: bytes = b"") -> MagicMock:
    resp = MagicMock()
    resp.status_code = status_code
    resp.content = content
    return resp


# ---------------------------------------------------------------------------
# URL generation
# ---------------------------------------------------------------------------


class TestVisionUrl:
    def test_spot_url(self) -> None:
        url = _vision_url("BTCUSDT", "2024-06-15", market="spot")
        assert url == (
            "https://data.binance.vision/data/spot/daily/aggTrades"
            "/BTCUSDT/BTCUSDT-aggTrades-2024-06-15.zip"
        )

    def test_um_futures_url(self) -> None:
        url = _vision_url("BTCUSDT", "2024-06-15", market="um_futures")
        assert url == (
            "https://data.binance.vision/data/futures/um/daily/aggTrades"
            "/BTCUSDT/BTCUSDT-aggTrades-2024-06-15.zip"
        )

    def test_default_is_spot(self) -> None:
        assert _vision_url("X", "2024-01-01") == _vision_url("X", "2024-01-01", market="spot")


# ---------------------------------------------------------------------------
# Cache key generation
# ---------------------------------------------------------------------------


class TestCacheKey:
    def test_spot_uses_plain_symbol(self) -> None:
        assert _cache_key("BTCUSDT", "spot") == "BTCUSDT"

    def test_um_futures_uses_prefix(self) -> None:
        assert _cache_key("BTCUSDT", "um_futures") == "BINANCE_UM_BTCUSDT"

    def test_um_futures_ethusdt(self) -> None:
        assert _cache_key("ETHUSDT", "um_futures") == "BINANCE_UM_ETHUSDT"


# ---------------------------------------------------------------------------
# Markets parsing
# ---------------------------------------------------------------------------


class TestParseMarkets:
    def test_spot_only(self) -> None:
        assert _parse_markets("spot") == ["spot"]

    def test_um_futures_only(self) -> None:
        assert _parse_markets("um_futures") == ["um_futures"]

    def test_both(self) -> None:
        assert _parse_markets("spot,um_futures") == ["spot", "um_futures"]

    def test_with_spaces(self) -> None:
        assert _parse_markets("spot , um_futures") == ["spot", "um_futures"]

    def test_empty_defaults_to_spot(self) -> None:
        assert _parse_markets("") == ["spot"]

    def test_invalid_raises(self) -> None:
        with pytest.raises(ValueError, match="Invalid market"):
            _parse_markets("binance_coin_futures")


# ---------------------------------------------------------------------------
# UM Futures CSV parsing (the core of Issue #278)
# ---------------------------------------------------------------------------


class TestFuturesCsvParsing:
    """Test _download_vision_day with UM futures CSV format."""

    # UM futures CSV: 7 columns, lowercase booleans, no was_best_price
    UM_CSV_NO_HEADER = (
        "100000001,43250.5,0.001,200000001,200000001,1700000000123,true\n"
        "100000002,43251.0,0.002,200000002,200000003,1700000000456,false\n"
    )

    UM_CSV_WITH_HEADER = (
        "agg_trade_id,price,quantity,first_trade_id,last_trade_id,transact_time,is_buyer_maker\n"
        "100000001,43250.5,0.001,200000001,200000001,1700000000123,true\n"
        "100000002,43251.0,0.002,200000002,200000003,1700000000456,false\n"
    )

    # Spot CSV: 8 columns, Title case booleans, has was_best_price
    SPOT_CSV = (
        "100000001,43250.50000000,0.00100000,200000001,200000001,1700000000123,True,True\n"
        "100000002,43251.00000000,0.00200000,200000002,200000003,1700000000456,False,True\n"
    )

    @patch("requests.get")
    def test_um_futures_no_header_pre_20220811(self, mock_get: MagicMock) -> None:
        """UM futures before 2022-08-11: no header, 7 columns."""
        date_str = "2022-08-10"  # Day before header cutoff
        content = _make_vision_zip("BTCUSDT", date_str, self.UM_CSV_NO_HEADER)
        mock_get.return_value = _mock_response(200, content)

        df = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="um_futures")

        assert df is not None
        assert df.columns == _UM_COLUMNS
        assert len(df) == 2
        assert df["agg_trade_id"][0] == 100000001
        assert df["price"][0] == pytest.approx(43250.5)
        assert df["is_buyer_maker"][0] is True
        assert df["is_buyer_maker"][1] is False
        assert df["timestamp"][0] == 1700000000123  # ms, NOT divided

    @patch("requests.get")
    def test_um_futures_with_header_post_20220811(self, mock_get: MagicMock) -> None:
        """UM futures from 2022-08-11: has header, 7 columns."""
        date_str = "2022-08-11"  # Exactly the cutoff date
        content = _make_vision_zip("BTCUSDT", date_str, self.UM_CSV_WITH_HEADER)
        mock_get.return_value = _mock_response(200, content)

        df = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="um_futures")

        assert df is not None
        assert df.columns == _UM_COLUMNS
        assert len(df) == 2
        assert df["agg_trade_id"][0] == 100000001
        assert df["timestamp"][0] == 1700000000123

    @patch("requests.get")
    def test_um_futures_timestamps_stay_ms(self, mock_get: MagicMock) -> None:
        """UM futures timestamps are always ms. No us normalization applied."""
        # Use a 2025 date (spot would have us timestamps, futures stay ms)
        date_str = "2025-06-15"
        csv = (
            "agg_trade_id,price,quantity,first_trade_id,last_trade_id,transact_time,is_buyer_maker\n"
            "999999999,100000.0,1.5,111111,111112,1750000000789,true\n"
        )
        content = _make_vision_zip("BTCUSDT", date_str, csv)
        mock_get.return_value = _mock_response(200, content)

        df = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="um_futures")

        assert df is not None
        # Should NOT divide by 1000 — this is ms, not us
        assert df["timestamp"][0] == 1750000000789

    @patch("requests.get")
    def test_spot_us_normalization_still_works(self, mock_get: MagicMock) -> None:
        """Spot data from 2025+ with us timestamps gets normalized to ms."""
        date_str = "2025-01-02"
        # Spot CSV with us timestamps (16 digits) and 8 columns
        csv = (
            "100000001,43250.50000000,0.00100000,200000001,200000001,1735776000123456,True,True\n"
        )
        content = _make_vision_zip("BTCUSDT", date_str, csv)
        mock_get.return_value = _mock_response(200, content)

        df = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="spot")

        assert df is not None
        # us timestamp should be divided by 1000
        assert df["timestamp"][0] == 1735776000123456 // 1000

    @patch("requests.get")
    def test_um_futures_variable_precision(self, mock_get: MagicMock) -> None:
        """UM futures can have variable decimal precision in price/quantity."""
        date_str = "2024-01-15"
        csv = (
            "agg_trade_id,price,quantity,first_trade_id,last_trade_id,transact_time,is_buyer_maker\n"
            "100000001,43250.5,0.001,200000001,200000001,1700000000123,true\n"
            "100000002,43251.123456789,10,200000002,200000003,1700000000456,false\n"
        )
        content = _make_vision_zip("BTCUSDT", date_str, csv)
        mock_get.return_value = _mock_response(200, content)

        df = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="um_futures")

        assert df is not None
        assert df["price"][0] == pytest.approx(43250.5)
        assert df["price"][1] == pytest.approx(43251.123456789)
        assert df["quantity"][1] == pytest.approx(10.0)

    @patch("requests.get")
    def test_um_futures_sorted_by_agg_trade_id(self, mock_get: MagicMock) -> None:
        """Output is always sorted by agg_trade_id."""
        date_str = "2024-01-15"
        # Intentionally reversed order (with header since post-cutoff)
        csv = (
            "agg_trade_id,price,quantity,first_trade_id,last_trade_id,transact_time,is_buyer_maker\n"
            "100000002,43251.0,0.002,200000002,200000003,1700000000456,false\n"
            "100000001,43250.5,0.001,200000001,200000001,1700000000123,true\n"
        )
        content = _make_vision_zip("BTCUSDT", date_str, csv)
        mock_get.return_value = _mock_response(200, content)

        df = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="um_futures")

        assert df is not None
        assert df["agg_trade_id"].to_list() == [100000001, 100000002]

    @patch("requests.get")
    def test_um_futures_schema_matches_spot(self, mock_get: MagicMock) -> None:
        """UM futures output schema is identical to spot output schema."""
        date_str = "2024-01-15"

        # Parse futures (with header since post-cutoff date)
        um_csv = (
            "agg_trade_id,price,quantity,first_trade_id,last_trade_id,transact_time,is_buyer_maker\n"
            "100000001,43250.5,0.001,200000001,200000001,1700000000123,true\n"
        )
        um_zip = _make_vision_zip("BTCUSDT", date_str, um_csv)
        mock_get.return_value = _mock_response(200, um_zip)
        df_um = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="um_futures")

        # Parse spot
        spot_csv = "100000001,43250.50000000,0.00100000,200000001,200000001,1700000000123,True,True\n"
        spot_zip = _make_vision_zip("BTCUSDT", date_str, spot_csv)
        mock_get.return_value = _mock_response(200, spot_zip)
        df_spot = _download_vision_day("BTCUSDT", date_str, timeout_s=10, market="spot")

        assert df_um is not None
        assert df_spot is not None
        # Futures has exactly 7 columns; spot may have extra (was_best_price → column_8)
        # The important thing is: selecting the canonical 7 columns gives identical schema
        canonical = _UM_COLUMNS
        assert df_um.columns == canonical
        df_spot_trimmed = df_spot.select(canonical)
        assert df_um.schema == df_spot_trimmed.schema


class TestUmHeaderCutoff:
    """Verify header cutoff date constant."""

    def test_cutoff_is_20220811(self) -> None:
        assert _UM_HEADER_CUTOFF == "2022-08-11"

    def test_date_comparison_works(self) -> None:
        """String comparison on YYYY-MM-DD works for date ordering."""
        assert _UM_HEADER_CUTOFF > "2022-08-10"
        assert _UM_HEADER_CUTOFF <= "2022-08-11"
        assert _UM_HEADER_CUTOFF <= "2024-01-01"
