"""Tests for the market data collector module."""

import json
import os
import tempfile

import pytest

import horizon as hz
from horizon._horizon import Collector, CollectorConfig, CollectorStatus


# ---------------------------------------------------------------------------
# Rust type imports
# ---------------------------------------------------------------------------


class TestRustTypes:
    def test_collector_config_importable(self):
        assert CollectorConfig is not None

    def test_collector_status_importable(self):
        assert CollectorStatus is not None

    def test_collector_importable(self):
        assert Collector is not None

    def test_config_defaults(self):
        cfg = CollectorConfig(market_ids=["abc"])
        assert cfg.exchange == "polymarket"
        assert cfg.orderbook_interval_secs == 5.0
        assert cfg.trade_poll_interval_secs == 10.0
        assert cfg.metadata_interval_secs == 300.0
        assert cfg.max_book_levels == 0
        assert cfg.db_path == "collector.db"
        assert cfg.market_ids == ["abc"]

    def test_config_custom(self):
        cfg = CollectorConfig(
            market_ids=["a", "b"],
            exchange="kalshi",
            orderbook_interval_secs=10.0,
            trade_poll_interval_secs=30.0,
            metadata_interval_secs=600.0,
            max_book_levels=5,
            db_path="/tmp/test.db",
        )
        assert cfg.exchange == "kalshi"
        assert cfg.orderbook_interval_secs == 10.0
        assert cfg.max_book_levels == 5
        assert len(cfg.market_ids) == 2

    def test_config_repr(self):
        cfg = CollectorConfig(market_ids=["a", "b"])
        r = repr(cfg)
        assert "CollectorConfig" in r
        assert "markets=2" in r

    def test_config_setters(self):
        cfg = CollectorConfig(market_ids=["a"])
        cfg.exchange = "kalshi"
        assert cfg.exchange == "kalshi"
        cfg.orderbook_interval_secs = 15.0
        assert cfg.orderbook_interval_secs == 15.0


# ---------------------------------------------------------------------------
# Python DataCollectorConfig
# ---------------------------------------------------------------------------


class TestDataCollectorConfig:
    def test_defaults(self):
        cfg = hz.DataCollectorConfig()
        assert cfg.markets == []
        assert cfg.exchange == "polymarket"
        assert cfg.orderbook_interval == 5.0
        assert cfg.retention_hours == 0.0

    def test_to_rust_config(self):
        cfg = hz.DataCollectorConfig(
            markets=["tok1", "tok2"],
            exchange="kalshi",
            orderbook_interval=10.0,
            db_path="/tmp/test.db",
        )
        rc = cfg.to_rust_config()
        assert isinstance(rc, CollectorConfig)
        assert rc.market_ids == ["tok1", "tok2"]
        assert rc.exchange == "kalshi"
        assert rc.orderbook_interval_secs == 10.0


# ---------------------------------------------------------------------------
# Collector instance (in-memory DB)
# ---------------------------------------------------------------------------


class TestCollector:
    def _make_collector(self):
        cfg = CollectorConfig(market_ids=[], db_path=":memory:")
        return Collector(cfg)

    def test_create(self):
        c = self._make_collector()
        assert c is not None

    def test_status_not_running(self):
        c = self._make_collector()
        st = c.status()
        assert isinstance(st, CollectorStatus)
        assert st.is_running is False
        assert st.ob_snapshots_total == 0
        assert st.trades_total == 0

    def test_status_repr(self):
        c = self._make_collector()
        r = repr(c.status())
        assert "CollectorStatus" in r

    def test_record_orderbook(self):
        c = self._make_collector()
        bids = json.dumps([[0.55, 100.0], [0.54, 200.0]])
        asks = json.dumps([[0.56, 150.0]])
        c.record_orderbook(
            "mkt1", "polymarket", bids, asks,
            0.55, 0.56, 0.555, 0.01, 1700000000.0,
        )
        st = c.status()
        assert st.ob_snapshots_total == 1

    def test_record_trades(self):
        c = self._make_collector()
        trades = json.dumps([
            {"trade_id": "t1", "price": 0.55, "size": 10, "side": "buy", "timestamp": 1700000000.0},
            {"trade_id": "t2", "price": 0.56, "size": 5, "side": "sell", "timestamp": 1700000001.0},
        ])
        count = c.record_trades(trades, "mkt1", "polymarket")
        assert count == 2
        assert c.status().trades_total == 2

    def test_trade_dedup(self):
        c = self._make_collector()
        trades = json.dumps([
            {"trade_id": "t1", "price": 0.55, "size": 10, "side": "buy", "timestamp": 1700000000.0},
        ])
        c.record_trades(trades, "mkt1", "polymarket")
        # Insert same trade again - should be ignored by INSERT OR IGNORE
        count = c.record_trades(trades, "mkt1", "polymarket")
        # The second insert may succeed (returns 1) since SQLite INSERT OR IGNORE
        # doesn't error but also doesn't insert duplicate
        total = c.status().trades_total
        assert total >= 1

    def test_query_orderbooks_empty(self):
        c = self._make_collector()
        raw = c.query_orderbooks("nonexistent")
        result = json.loads(raw)
        assert result == []

    def test_query_orderbooks_roundtrip(self):
        c = self._make_collector()
        bids = json.dumps([[0.55, 100.0]])
        asks = json.dumps([[0.56, 50.0]])
        c.record_orderbook("mkt1", "poly", bids, asks, 0.55, 0.56, 0.555, 0.01, 1700000000.0)

        raw = c.query_orderbooks("mkt1", 0.0, 2000000000.0, 10)
        result = json.loads(raw)
        assert len(result) == 1
        assert result[0]["market_id"] == "mkt1"
        assert result[0]["best_bid"] == 0.55
        assert result[0]["best_ask"] == 0.56
        assert len(result[0]["bids"]) == 1

    def test_query_trades_empty(self):
        c = self._make_collector()
        raw = c.query_trades("nonexistent")
        result = json.loads(raw)
        assert result == []

    def test_query_trades_roundtrip(self):
        c = self._make_collector()
        trades = json.dumps([
            {"trade_id": "t1", "price": 0.55, "size": 10, "side": "buy", "timestamp": 1700000000.0},
        ])
        c.record_trades(trades, "mkt1", "polymarket")

        raw = c.query_trades("mkt1", 0.0, 2000000000.0, 10)
        result = json.loads(raw)
        assert len(result) == 1
        assert result[0]["price"] == 0.55

    def test_query_metadata_none(self):
        c = self._make_collector()
        result = c.query_metadata("nonexistent")
        assert result is None

    def test_purge(self):
        c = self._make_collector()
        deleted = c.purge(0.001)  # very small age - would delete everything
        assert deleted >= 0

    def test_start_empty_markets_raises(self):
        cfg = CollectorConfig(market_ids=[], db_path=":memory:")
        c = Collector(cfg)
        with pytest.raises(ValueError):
            c.start()


# ---------------------------------------------------------------------------
# Pipeline factory
# ---------------------------------------------------------------------------


class TestPipelineCollector:
    def test_factory_returns_callable(self):
        fn = hz.collector("test_feed")
        assert callable(fn)

    def test_factory_with_params(self):
        fn = hz.collector("test_feed", db_path=":memory:", orderbook_every_n=5)
        assert callable(fn)


# ---------------------------------------------------------------------------
# Standalone collect() validation
# ---------------------------------------------------------------------------


class TestCollectDaemon:
    def test_empty_markets_raises(self):
        with pytest.raises(ValueError, match="must not be empty"):
            hz.collect(markets=[], exchange="polymarket")

    def test_config_empty_markets_raises(self):
        cfg = hz.DataCollectorConfig(markets=[])
        with pytest.raises(ValueError, match="must not be empty"):
            hz.collect(config=cfg)


# ---------------------------------------------------------------------------
# Parquet export
# ---------------------------------------------------------------------------


class TestParquetExport:
    def test_roundtrip(self):
        pytest.importorskip("pyarrow")

        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "test.db")
            cfg = CollectorConfig(market_ids=[], db_path=db_path)
            c = Collector(cfg)

            # Insert some data
            bids = json.dumps([[0.55, 100.0]])
            asks = json.dumps([[0.56, 50.0]])
            c.record_orderbook("mkt1", "poly", bids, asks, 0.55, 0.56, 0.555, 0.01, 1700000000.0)

            trades = json.dumps([
                {"trade_id": "t1", "price": 0.55, "size": 10, "side": "buy", "timestamp": 1700000000.0},
            ])
            c.record_trades(trades, "mkt1", "polymarket")

            # Export
            parquet_dir = os.path.join(tmpdir, "parquet")
            files = hz.export_parquet(db_path, parquet_dir)
            assert len(files) >= 1
            for f in files:
                assert os.path.exists(f)
                assert f.endswith(".parquet")

    def test_missing_pyarrow(self):
        # Can't easily test ImportError without uninstalling pyarrow,
        # so just verify the function exists
        assert callable(hz.export_parquet)


# ---------------------------------------------------------------------------
# Query helpers
# ---------------------------------------------------------------------------


class TestQueryHelpers:
    def test_query_orderbooks_empty_db(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "test.db")
            # Create DB by making a collector
            Collector(CollectorConfig(market_ids=[], db_path=db_path))
            result = hz.query_orderbooks(db_path, "nonexistent")
            assert result == []

    def test_query_collected_trades_empty_db(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "test.db")
            Collector(CollectorConfig(market_ids=[], db_path=db_path))
            result = hz.query_collected_trades(db_path, "nonexistent")
            assert result == []

    def test_query_orderbooks_roundtrip(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "test.db")
            cfg = CollectorConfig(market_ids=[], db_path=db_path)
            c = Collector(cfg)

            bids = json.dumps([[0.55, 100.0]])
            asks = json.dumps([[0.56, 50.0]])
            c.record_orderbook("mkt1", "poly", bids, asks, 0.55, 0.56, 0.555, 0.01, 1700000000.0)

            result = hz.query_orderbooks(db_path, "mkt1")
            assert len(result) == 1
            assert result[0]["best_bid"] == 0.55

    def test_query_collected_trades_roundtrip(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "test.db")
            cfg = CollectorConfig(market_ids=[], db_path=db_path)
            c = Collector(cfg)

            trades = json.dumps([
                {"trade_id": "t1", "price": 0.60, "size": 5, "side": "sell", "timestamp": 1700000000.0},
            ])
            c.record_trades(trades, "mkt1", "polymarket")

            result = hz.query_collected_trades(db_path, "mkt1")
            assert len(result) == 1
            assert result[0]["price"] == 0.60


# ---------------------------------------------------------------------------
# Re-exports
# ---------------------------------------------------------------------------


class TestExports:
    def test_all_exports(self):
        assert "Collector" in hz.__all__
        assert "CollectorConfig" in hz.__all__
        assert "CollectorStatus" in hz.__all__
        assert "DataCollectorConfig" in hz.__all__
        assert "collect" in hz.__all__
        assert "collector" in hz.__all__
        assert "export_parquet" in hz.__all__
        assert "query_orderbooks" in hz.__all__
        assert "query_collected_trades" in hz.__all__
