"""Tests for Issue #158 Phase 8: overlap_strategy routing.

Validates that "replace" routes to store_bars_replacing(), invalid strategies
raise ValueError, and existing strategies ("off", "warn", "skip") are unaffected.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest


def test_invalid_strategy_raises():
    """Invalid overlap_strategy string raises ValueError."""
    from opendeviationbar.clickhouse.constants import validate_overlap_strategy

    with pytest.raises(ValueError, match="Invalid overlap_strategy='bogus'"):
        validate_overlap_strategy("bogus")

    with pytest.raises(ValueError, match="Invalid overlap_strategy=''"):
        validate_overlap_strategy("")


def test_valid_strategies_pass():
    """All four valid strategies pass validation."""
    from opendeviationbar.clickhouse.constants import validate_overlap_strategy

    for strategy in ("off", "warn", "skip", "replace"):
        assert validate_overlap_strategy(strategy) == strategy


def _make_cache():
    """Create a fake cache with both BulkStore and Maintenance mixins."""
    from opendeviationbar.clickhouse.bulk_operations import BulkStoreMixin
    from opendeviationbar.clickhouse.maintenance import MaintenanceMixin

    class FakeCache(BulkStoreMixin, MaintenanceMixin):
        pass

    return FakeCache()


def test_replace_routes_to_store_bars_replacing():
    """store_bars_batch(overlap_strategy='replace') delegates to store_bars_replacing()."""
    import polars as pl

    cache = _make_cache()
    bars = pl.DataFrame({
        "close_time_ms": [1700000000000],
        "open": [100.0],
        "high": [101.0],
        "low": [99.0],
        "close": [100.5],
        "volume": [10.0],
        "first_agg_trade_id": [1000],
        "last_agg_trade_id": [2000],
    })

    with patch.object(cache, "store_bars_replacing", return_value=1) as mock_replace:
        result = cache.store_bars_batch(
            "BTCUSDT", 250, bars, overlap_strategy="replace",
        )

    assert result == 1
    mock_replace.assert_called_once_with(
        "BTCUSDT", 250, bars,
        ouroboros_mode=None, version=None, _skip_lock=False,
    )


def test_replace_pandas_raises():
    """store_bars_bulk(overlap_strategy='replace') raises ValueError (requires Polars)."""
    import pandas as pd

    cache = _make_cache()
    bars = pd.DataFrame({
        "close_time_ms": [1700000000000],
        "open": [100.0],
        "high": [101.0],
        "low": [99.0],
        "close": [100.5],
        "volume": [10.0],
    })

    with pytest.raises(ValueError, match="requires Polars DataFrame"):
        cache.store_bars_bulk("BTCUSDT", 250, bars, overlap_strategy="replace")


def test_warn_skip_off_unchanged():
    """Existing strategies don't route to store_bars_replacing."""
    import polars as pl

    cache = _make_cache()
    cache.client = MagicMock()
    cache.client.insert_arrow = MagicMock(return_value=MagicMock(written_rows=1))
    cache.client.query = MagicMock(return_value=MagicMock(result_rows=[]))
    cache._guard_ouroboros_mode_consistency = MagicMock()

    bars = pl.DataFrame({
        "close_time_ms": [1700000000000],
        "open": [100.0],
        "high": [101.0],
        "low": [99.0],
        "close": [100.5],
        "volume": [10.0],
    })

    with patch.object(cache, "store_bars_replacing") as mock_replace:
        for strategy in ("off", "warn"):
            with patch("opendeviationbar.clickhouse.write_lock.cache_write_lock") as mock_lock:
                mock_lock.return_value.__enter__ = MagicMock(return_value=None)
                mock_lock.return_value.__exit__ = MagicMock(return_value=False)
                cache.store_bars_batch(
                    "BTCUSDT", 250, bars,
                    overlap_strategy=strategy,
                    ouroboros_mode="day",
                )

    mock_replace.assert_not_called()


def test_no_recursion_on_replace():
    """store_bars_replacing's inner call uses overlap_strategy='off', not 'replace'."""
    import polars as pl

    cache = _make_cache()
    cache.client = MagicMock()
    cache.client.command = MagicMock()
    # result_rows=[] → no stale left-straddling bar → _cleanup_stale_left_straddle is a no-op
    cache.client.query = MagicMock(return_value=MagicMock(result_rows=[]))
    cache.client.insert_arrow = MagicMock(return_value=MagicMock(written_rows=5))
    cache._guard_ouroboros_mode_consistency = MagicMock()

    bars = pl.DataFrame({
        "close_time_ms": [1700000000000],
        "open": [100.0],
        "high": [101.0],
        "low": [99.0],
        "close": [100.5],
        "volume": [10.0],
        "first_agg_trade_id": [1000],
        "last_agg_trade_id": [2000],
    })

    with patch("opendeviationbar.clickhouse.write_lock.cache_write_lock") as mock_lock:
        mock_lock.return_value.__enter__ = MagicMock(return_value=None)
        mock_lock.return_value.__exit__ = MagicMock(return_value=False)

        # This should NOT recurse: store_bars_replacing → store_bars_batch(overlap_strategy="off")
        result = cache.store_bars_replacing(
            "BTCUSDT", 250, bars, ouroboros_mode="day",
        )

    assert result == 5
    # Verify the broad-range DELETE was issued (command called at least once;
    # _cleanup_stale_left_straddle is a no-op when result_rows=[] so no second DELETE)
    assert cache.client.command.call_count >= 1
    assert any("DELETE" in str(c) for c in cache.client.command.call_args_list)


def test_cleanup_stale_left_straddle_no_op_when_clean():
    """_cleanup_stale_left_straddle issues no DELETE when predecessor ends before anchor."""
    cache = _make_cache()
    cache.client = MagicMock()
    # Predecessor ends at tid 999, anchor is 1000 — no straddle
    cache.client.query = MagicMock(return_value=MagicMock(result_rows=[(800, 999)]))

    cache._cleanup_stale_left_straddle("BTCUSDT", 250, "day", new_first=1000)

    cache.client.command.assert_not_called()


def test_cleanup_stale_left_straddle_deletes_straddling_bar():
    """_cleanup_stale_left_straddle deletes a bar that straddles the repair anchor.

    Issue #299: async lightweight DELETE race — large sidecar bar (first=800, last=1050)
    hasn't been masked yet when _do_replace inserts bars starting at first=1000.
    The bar straddles the anchor (last=1050 >= new_first=1000) → must be deleted.
    """
    cache = _make_cache()
    cache.client = MagicMock()
    # Predecessor: first=800, last=1050 — straddles anchor at 1000
    cache.client.query = MagicMock(return_value=MagicMock(result_rows=[(800, 1050)]))

    cache._cleanup_stale_left_straddle("BTCUSDT", 250, "day", new_first=1000)

    assert cache.client.command.call_count == 1
    delete_sql = cache.client.command.call_args[0][0]
    assert "DELETE FROM" in delete_sql
    assert "first_agg_trade_id = 800" in delete_sql
    assert "last_agg_trade_id = 1050" in delete_sql
