"""
Tests for signalab.data.market_data.MarketData
"""

from __future__ import annotations

import tempfile
from pathlib import Path
from typing import cast

import numpy as np
import pandas as pd
import pytest
from conftest import make_market_data, make_ticks

from signalab.data.market_data import MarketData, ValueTimestampResolution

# ─────────────────────────────────────────────
# Candle construction
# ─────────────────────────────────────────────


class TestCandleComputation:
    def test_candles_1m_ohlc_columns(self, market_data: MarketData) -> None:
        c: pd.DataFrame = market_data.candles_1m
        for col in (
            "bid_open",
            "bid_high",
            "bid_low",
            "bid_close",
            "ask_open",
            "ask_high",
            "ask_low",
            "ask_close",
        ):
            assert col in c.columns, f"Missing column: {col}"

    def test_candles_1m_high_gte_low(self, market_data: MarketData) -> None:
        c: pd.DataFrame = market_data.candles_1m
        assert (c["bid_high"] >= c["bid_low"]).all()
        assert (c["ask_high"] >= c["ask_low"]).all()

    def test_candles_1m_high_gte_open_close(self, market_data: MarketData) -> None:
        c: pd.DataFrame = market_data.candles_1m
        assert (c["bid_high"] >= c["bid_open"]).all()
        assert (c["bid_high"] >= c["bid_close"]).all()

    def test_candles_1m_low_lte_open_close(self, market_data: MarketData) -> None:
        c: pd.DataFrame = market_data.candles_1m
        assert (c["bid_low"] <= c["bid_open"]).all()
        assert (c["bid_low"] <= c["bid_close"]).all()

    def test_volume_summed(self) -> None:
        ticks: pd.DataFrame = make_ticks(n=120)
        md = MarketData(ticks)
        candles: pd.DataFrame = md.candles_1m
        # _compute_candles resamples volume with no label/closed args (defaults to left/left)
        expected: pd.Series = ticks["volume"].resample("1min").sum()
        shared_idx = candles.index.intersection(expected.index)
        pd.testing.assert_series_equal(
            candles.loc[shared_idx, "volume"],
            expected.loc[shared_idx],
            check_names=False,
        )

    def test_spread_is_mean(self) -> None:
        ticks: pd.DataFrame = make_ticks(n=120)
        md = MarketData(ticks)
        candles: pd.DataFrame = md.candles_1m
        # _compute_candles resamples spread with no label/closed args (defaults to left/left)
        expected: pd.Series = ticks["spread"].resample("1min").mean()
        shared_idx = candles.index.intersection(expected.index)
        pd.testing.assert_series_equal(
            candles.loc[shared_idx, "spread"],
            expected.loc[shared_idx],
            check_names=False,
        )

    def test_candles_cached(self, market_data: MarketData) -> None:
        c1: pd.DataFrame = market_data.candles_1m
        c2: pd.DataFrame = market_data.candles_1m
        assert c1 is c2  # same object — not recomputed

    def test_multiple_timeframes_available(self, market_data: MarketData) -> None:
        md = make_market_data(n=500)
        assert len(md.candles_5m) > 0
        assert len(md.candles_15m) > 0

    def test_ask_always_gt_bid_in_candles(self, market_data: MarketData) -> None:
        c: pd.DataFrame = market_data.candles_1m
        assert (c["ask_open"] > c["bid_open"]).all()
        assert (c["ask_close"] > c["bid_close"]).all()


# ─────────────────────────────────────────────
# filter()
# ─────────────────────────────────────────────


class TestFilter:
    def test_filter_from_date(self, market_data: MarketData) -> None:
        cutoff: pd.Timestamp = market_data.ticks.index[50]
        filtered: MarketData = market_data.filter(from_date=cutoff)
        assert filtered.ticks.index.min() >= cutoff

    def test_filter_to_date(self, market_data: MarketData) -> None:
        cutoff: pd.Timestamp = market_data.ticks.index[100]
        filtered: MarketData = market_data.filter(to_date=cutoff)
        assert filtered.ticks.index.max() <= cutoff

    def test_filter_range(self, market_data: MarketData) -> None:
        lo: pd.Timestamp = market_data.ticks.index[20]
        hi: pd.Timestamp = market_data.ticks.index[80]
        filtered: MarketData = market_data.filter(from_date=lo, to_date=hi)
        assert filtered.ticks.index.min() >= lo
        assert filtered.ticks.index.max() <= hi

    def test_filter_does_not_mutate_original(self, market_data: MarketData) -> None:
        original_len: int = len(market_data.ticks)
        cutoff: pd.Timestamp = market_data.ticks.index[50]
        market_data.filter(from_date=cutoff)
        assert len(market_data.ticks) == original_len

    def test_filter_empty_range(self, market_data: MarketData) -> None:
        future = pd.Timestamp("2099-01-01")
        filtered: MarketData = market_data.filter(from_date=future)
        assert filtered.ticks.empty


# ─────────────────────────────────────────────
# query() and query_value()
# ─────────────────────────────────────────────


class TestQuery:
    def test_query_ticks(self, market_data: MarketData) -> None:
        result: pd.DataFrame = market_data.query("ticks")
        pd.testing.assert_frame_equal(result, market_data.ticks)

    def test_query_candles(self, market_data: MarketData) -> None:
        result: pd.DataFrame = market_data.query("1m")
        pd.testing.assert_frame_equal(result, market_data.candles_1m)

    def test_query_unknown_raises(self, market_data: MarketData) -> None:
        with pytest.raises(ValueError, match="Unknown frequency"):
            market_data.query("3m")

    def test_query_value_exact(self, market_data: MarketData) -> None:
        ts: pd.Timestamp = market_data.ticks.index[10]
        val: float = market_data.query_value(
            "ticks", "bid", ts, ValueTimestampResolution.EXACT
        )
        assert val == market_data.ticks.iloc[10]["bid"]

    def test_query_value_closest_before(self, market_data: MarketData) -> None:
        ts: pd.Timestamp = market_data.ticks.index[10]
        between: pd.Timestamp = ts + pd.Timedelta(seconds=5)
        val: float = market_data.query_value(
            "ticks", "bid", between, ValueTimestampResolution.CLOSEST_BEFORE
        )
        assert val == market_data.ticks.iloc[10]["bid"]

    def test_query_value_missing_column_raises(self, market_data: MarketData) -> None:
        ts: pd.Timestamp = market_data.ticks.index[0]
        with pytest.raises(ValueError, match="Column 'nonexistent'"):
            market_data.query_value("ticks", "nonexistent", ts)


# ─────────────────────────────────────────────
# CSV loading
# ─────────────────────────────────────────────


class TestLoadFromCSV:
    def _write_csv(self, df: pd.DataFrame, path: Path) -> None:
        df_out: pd.DataFrame = df.copy()
        df_out.index.name = "timestamp"
        df_out.to_csv(path)

    def test_load_single_file(self) -> None:
        ticks: pd.DataFrame = make_ticks(n=50)
        with tempfile.TemporaryDirectory() as tmp:
            p = Path(tmp) / "ticks.csv"
            self._write_csv(ticks[["bid", "ask", "volume"]], p)
            md = MarketData.load_from_ticks([p])
        assert len(md.ticks) == 50
        assert "bid" in md.ticks.columns
        assert "ask" in md.ticks.columns

    def test_load_multiple_files_sorted(self) -> None:
        t1: pd.DataFrame = make_ticks(n=50, start="2024-01-01", seed=1)
        t2: pd.DataFrame = make_ticks(n=50, start="2024-01-02", seed=2)
        with tempfile.TemporaryDirectory() as tmp:
            p1 = Path(tmp) / "a.csv"
            p2 = Path(tmp) / "b.csv"
            self._write_csv(t1[["bid", "ask", "volume"]], p1)
            self._write_csv(t2[["bid", "ask", "volume"]], p2)
            md = MarketData.load_from_ticks([p2, p1])  # deliberately reversed
        assert md.ticks.index.is_monotonic_increasing

    def test_load_deduplicates_overlapping_files(self) -> None:
        """Regression: duplicate timestamps from overlapping CSV files must be dropped."""
        ticks: pd.DataFrame = make_ticks(n=50)
        with tempfile.TemporaryDirectory() as tmp:
            p = Path(tmp) / "ticks.csv"
            self._write_csv(ticks[["bid", "ask", "volume"]], p)
            md = MarketData.load_from_ticks([p, p])  # same file twice
        assert len(md.ticks) == 50

    def test_spread_column_added_on_load(self) -> None:
        ticks: pd.DataFrame = make_ticks(n=20)
        with tempfile.TemporaryDirectory() as tmp:
            p = Path(tmp) / "ticks.csv"
            self._write_csv(ticks[["bid", "ask"]], p)
            md = MarketData.load_from_ticks([p])
        assert "spread" in md.ticks.columns
        np.testing.assert_allclose(
            cast(np.ndarray, md.ticks["spread"].values),
            cast(np.ndarray, md.ticks["ask"].values)
            - cast(np.ndarray, md.ticks["bid"].values),
            atol=1e-9,
        )
