"""Tests for Exness tick cache seeder (Phase 02).

Covers EXNS-01 through EXNS-07 from 02-RESEARCH.md test map.
"""

from __future__ import annotations

import io
import os
import zipfile
from datetime import date
from pathlib import Path
from unittest.mock import MagicMock, patch

import polars as pl
import pytest

# Ensure symbol gate is off for tests (some tests check registry directly)
os.environ.setdefault("OPENDEVIATIONBAR_SYMBOL_GATE", "off")

# Import seeder functions after env setup
sys_path_added = False


def _import_seeder():
    """Import seeder module functions."""
    import importlib
    import sys

    repo_root = Path(__file__).parent.parent
    scripts_dir = str(repo_root / "scripts")
    if scripts_dir not in sys.path:
        sys.path.insert(0, scripts_dir)

    # Import the module
    if "exness_tick_cache_seeder" in sys.modules:
        return importlib.reload(sys.modules["exness_tick_cache_seeder"])
    import exness_tick_cache_seeder
    return exness_tick_cache_seeder


def _make_exness_csv_zip(rows: list[str]) -> bytes:
    """Create a ZIP containing an Exness-format CSV with header."""
    header = '"Exness","Symbol","Timestamp","Bid","Ask"'
    csv_content = header + "\n" + "\n".join(rows)
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("Exness_EURUSD_Raw_Spread_2024_01_15.csv", csv_content)
    return buf.getvalue()


class TestExnessUrl:
    """Test _exness_url construction."""

    def test_exness_url_construction(self):
        """Verify URL has correct components with zero-padded date."""
        mod = _import_seeder()
        url = mod._exness_daily_url("EURUSD", date(2024, 1, 15))
        assert url == (
            "https://ticks.ex2archive.com/ticks/EURUSD_Raw_Spread"
            "/2024/01/15"
            "/Exness_EURUSD_Raw_Spread_2024_01_15.zip"
        )

    def test_exness_url_no_leading_zeros_dropped(self):
        """Verify months and days are zero-padded."""
        mod = _import_seeder()
        url = mod._exness_daily_url("XAUUSD", date(2022, 3, 5))
        assert "/2022/03/05/" in url
        assert "Exness_XAUUSD_Raw_Spread_2022_03_05.zip" in url


class TestDownloadParseCsv:
    """EXNS-01: Download + parse ZIP -> 3-column Polars DF."""

    @patch("requests.get")
    def test_download_parse_csv(self, mock_get):
        """Mock requests.get to return a ZIP with Exness CSV, verify 3-col schema."""
        mod = _import_seeder()

        csv_rows = [
            '"Exness","EURUSD","2024-01-15 00:00:00.032Z","1.08765","1.08775"',
            '"Exness","EURUSD","2024-01-15 00:00:00.155Z","1.08766","1.08776"',
            '"Exness","EURUSD","2024-01-15 00:00:00.288Z","1.08764","1.08774"',
        ]
        zip_bytes = _make_exness_csv_zip(csv_rows)

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.content = zip_bytes
        mock_get.return_value = mock_resp

        df = mod._download_exness_day("EURUSD", date(2024, 1, 15), timeout_s=10)

        assert df is not None
        assert df.columns == ["timestamp", "bid", "ask"]
        assert df.shape[0] == 3
        assert df["timestamp"].dtype == pl.Int64
        assert df["bid"].dtype == pl.Float64
        assert df["ask"].dtype == pl.Float64

        # Verify timestamps are in ms epoch (should be ~1705276800xxx)
        ts_values = df["timestamp"].to_list()
        for ts in ts_values:
            assert ts > 1_700_000_000_000_000  # us epoch for 2024
            assert ts < 1_800_000_000_000_000

        # Verify sorted by timestamp
        assert ts_values == sorted(ts_values)

        # Verify bid/ask values
        assert df["bid"][0] == pytest.approx(1.08765)
        assert df["ask"][0] == pytest.approx(1.08775)


class TestWeekend404:
    """EXNS-02: Weekend 404 returns None (not error)."""

    @patch("requests.get")
    def test_weekend_404_returns_none(self, mock_get):
        """404 response returns None without raising."""
        mod = _import_seeder()

        mock_resp = MagicMock()
        mock_resp.status_code = 404
        mock_get.return_value = mock_resp

        result = mod._download_exness_day("EURUSD", date(2024, 1, 13), timeout_s=10)
        assert result is None


class TestRegistryFilter:
    """EXNS-03: Registry filter returns only Exness symbols."""

    def test_registry_filter_exness_only(self):
        """Filter get_symbol_entries() by exchange='exness', no Binance leaks."""
        from opendeviationbar.symbol_registry import get_symbol_entries

        entries = get_symbol_entries()
        exness_symbols = {
            name: entry
            for name, entry in entries.items()
            if entry.exchange == "exness" and entry.enabled
        }

        # If Exness symbols exist in registry, verify no Binance
        if exness_symbols:
            for name, entry in exness_symbols.items():
                assert entry.exchange == "exness"
                assert entry.asset_class == "forex"
            # Verify no Binance symbol leaked through
            binance_symbols = {
                name for name, entry in entries.items()
                if entry.exchange == "binance"
            }
            assert not (set(exness_symbols) & binance_symbols)
        else:
            # If Plan 01 hasn't added Exness symbols yet, just verify the filter works
            # with the empty result (the filter logic is correct either way)
            assert len(exness_symbols) == 0


class TestParquetWrite:
    """EXNS-04: Parquet write produces valid 3-column file."""

    def test_parquet_write_3_columns(self, tmp_path):
        """Create a DataFrame, write via _atomic_write_parquet, verify schema."""
        from opendeviationbar.storage.parquet_io import _atomic_write_parquet

        df = pl.DataFrame({
            "timestamp": [1705276800032, 1705276800155, 1705276800288],
            "bid": [1.08765, 1.08766, 1.08764],
            "ask": [1.08775, 1.08776, 1.08774],
        }).cast({"timestamp": pl.Int64, "bid": pl.Float64, "ask": pl.Float64})

        parquet_path = tmp_path / "EXNESS_RAW_SPREAD_EURUSD" / "2024-01-15.parquet"
        parquet_path.parent.mkdir(parents=True, exist_ok=True)

        _atomic_write_parquet(df, parquet_path)

        assert parquet_path.exists()

        # Read back and verify
        df_read = pl.read_parquet(parquet_path)
        assert df_read.columns == ["timestamp", "bid", "ask"]
        assert df_read.shape[0] == 3
        assert df_read["timestamp"].dtype == pl.Int64
        assert df_read["bid"].dtype == pl.Float64
        assert df_read["ask"].dtype == pl.Float64
        assert df_read["bid"][0] == pytest.approx(1.08765)


class TestIdempotentSkip:
    """EXNS-05: Idempotent -- skips already-cached days."""

    @patch("requests.get")
    def test_idempotent_skip(self, mock_get, tmp_path):
        """If Parquet exists for a day, _seed_symbol should not re-download it."""
        mod = _import_seeder()

        cache_key = "EXNESS_RAW_SPREAD_EURUSD"
        ticks_dir = tmp_path / "ticks"

        # Create existing Parquet files for a range of days
        day_dir = ticks_dir / cache_key
        day_dir.mkdir(parents=True, exist_ok=True)

        # Create files for 2024-01-15 through yesterday
        from datetime import UTC, datetime, timedelta
        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)

        # Seed all days from start to yesterday so nothing is missing
        current = date(2024, 1, 15)
        while current <= yesterday:
            p = day_dir / f"{current.isoformat()}.parquet"
            df = pl.DataFrame({
                "timestamp": [1705276800000],
                "bid": [1.08765],
                "ask": [1.08775],
            })
            df.write_parquet(p)
            current += timedelta(days=1)

        state_path = tmp_path / "state.txt"

        count = mod._seed_symbol(
            symbol="EURUSD",
            cache_key="EXNESS_RAW_SPREAD_EURUSD",
            start_date=date(2024, 1, 15),
            ticks_dir=ticks_dir,
            max_workers=1,
            timeout_s=10,
            state_path=state_path,
            run_start_iso="2024-01-16T00:00:00Z",
        )

        # All days already cached -- nothing should be downloaded
        assert count.days_seeded == 0
        mock_get.assert_not_called()


class TestStateFile:
    """EXNS-06: State file written on completion."""

    @patch("opendeviationbar.notify.telegram.send_telegram", return_value=True)
    @patch("opendeviationbar.symbol_registry.get_symbol_entries")
    @patch("opendeviationbar.symbol_registry.get_effective_start_date", return_value="2024-01-01")
    def test_state_file_written(self, mock_start, mock_entries, mock_tg, tmp_path):
        """Run main() with mocked downloads, verify state file contents."""
        import types

        mod = _import_seeder()

        # Create a mock SymbolEntry for one Exness symbol
        mock_entry = MagicMock()
        mock_entry.exchange = "exness"
        mock_entry.enabled = True

        mock_entries.return_value = types.MappingProxyType({"EURUSD": mock_entry})

        state_file = tmp_path / "state.txt"
        ticks_dir = tmp_path / "ticks"
        ticks_dir.mkdir(parents=True, exist_ok=True)

        with patch(
            "opendeviationbar.config.exness_seeder.ExnessSeederConfig"
        ) as MockCfg, patch(
            "opendeviationbar.storage.parquet_io.get_cache_dir",
            return_value=tmp_path,
        ), patch.object(
            mod, "_seed_symbol", return_value=mod.SymbolSeedResult(symbol="EURUSD"),
        ):
            mock_cfg = MockCfg.return_value
            mock_cfg.historical_start = "2024-01-01"
            mock_cfg.max_concurrent_downloads = 1
            mock_cfg.download_timeout_s = 10
            mock_cfg.state_file = str(state_file)

            exit_code = mod.main()

        assert exit_code == 0
        assert state_file.exists()
        content = state_file.read_text()
        assert "ok\n" in content
        assert "timestamp=" in content
        assert "failed=0" in content


class TestRegistryEntriesCount:
    """EXNS-07: Verify Exness entries in symbols.toml."""

    def test_registry_entries_count(self):
        """Check Exness entries have correct fields."""
        from opendeviationbar.symbol_registry import get_symbol_entries

        entries = get_symbol_entries()
        exness_entries = {
            name: entry
            for name, entry in entries.items()
            if entry.exchange == "exness"
        }

        # If Plan 01 has added entries, verify correctness
        if exness_entries:
            for name, entry in exness_entries.items():
                assert entry.asset_class == "forex", f"{name} should be forex"
                assert entry.market == "raw_spread", f"{name} should be raw_spread"
            # Should be exactly 10 when Plan 01 completes
            if len(exness_entries) == 10:
                expected_symbols = {
                    "EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD",
                    "NZDUSD", "EURGBP", "EURJPY", "GBPJPY", "XAUUSD",
                }
                assert set(exness_entries.keys()) == expected_symbols
        else:
            # Plan 01 hasn't run yet -- this is expected in parallel execution
            pytest.skip("Exness symbols not yet in registry (Plan 01 dependency)")
