"""Day-mode invariant tests: no bar spans UTC midnight (#264)."""

import pytest
from opendeviationbar.validation.day_mode.guards import (
    split_trades_by_day,
    validate_bar_day_mode,
    validate_bars_day_mode,
)


class TestSplitTradesByDay:
    def test_empty_list(self):
        assert split_trades_by_day([]) == []

    def test_single_day(self):
        trades = [{"timestamp": 1704067200000 + i * 1000} for i in range(10)]
        groups = split_trades_by_day(trades)
        assert len(groups) == 1
        assert len(groups[0]) == 10

    def test_two_days(self):
        day1 = [{"timestamp": 1704067200000 + i * 1000} for i in range(5)]
        day2 = [{"timestamp": 1704153600000 + i * 1000} for i in range(3)]
        groups = split_trades_by_day(day1 + day2)
        assert len(groups) == 2
        assert len(groups[0]) == 5
        assert len(groups[1]) == 3

    def test_midnight_boundary_exact(self):
        """Trade at exactly midnight belongs to new day."""
        trades = [
            {"timestamp": 1704153599999},  # 2024-01-01 23:59:59.999
            {"timestamp": 1704153600000},  # 2024-01-02 00:00:00.000
        ]
        groups = split_trades_by_day(trades)
        assert len(groups) == 2

    def test_three_days(self):
        trades = [
            {"timestamp": 1704067200000},
            {"timestamp": 1704153600000},
            {"timestamp": 1704240000000},
        ]
        groups = split_trades_by_day(trades)
        assert len(groups) == 3

    def test_time_key_fallback(self):
        trades = [{"time": 1704067200000}]
        groups = split_trades_by_day(trades)
        assert len(groups) == 1


class TestValidateBarDayMode:
    def test_valid_bar(self):
        bar = {"open_time_ms": 1704067200000, "close_time_ms": 1704110400000}
        assert validate_bar_day_mode(bar) is True

    def test_cross_midnight_strict(self):
        bar = {"open_time_ms": 1704150000000, "close_time_ms": 1704160000000}
        with pytest.raises(AssertionError, match="Cross-midnight"):
            validate_bar_day_mode(bar, strict=True)

    def test_cross_midnight_lenient(self):
        bar = {"open_time_ms": 1704150000000, "close_time_ms": 1704160000000}
        assert validate_bar_day_mode(bar, strict=False) is False

    def test_batch_validation(self):
        bars = [
            {"open_time_ms": 1704067200000, "close_time_ms": 1704110400000},  # OK
            {"open_time_ms": 1704150000000, "close_time_ms": 1704160000000},  # BAD
        ]
        assert validate_bars_day_mode(bars, strict=False) == 1

    def test_same_day_boundary(self):
        """Bar that opens and closes on same day is valid."""
        # 2024-01-01 00:00:00 to 2024-01-01 23:59:59.999
        bar = {"open_time_ms": 1704067200000, "close_time_ms": 1704153599999}
        assert validate_bar_day_mode(bar) is True

    def test_exactly_at_midnight_is_next_day(self):
        """Bar opening at 23:59 and closing at exactly 00:00 next day is cross-midnight."""
        bar = {"open_time_ms": 1704153540000, "close_time_ms": 1704153600000}
        with pytest.raises(AssertionError, match="Cross-midnight"):
            validate_bar_day_mode(bar, strict=True)


class TestSplitTradesByDayEdgeCases:
    def test_all_same_timestamp(self):
        trades = [{"timestamp": 1704067200000}] * 5
        groups = split_trades_by_day(trades)
        assert len(groups) == 1
        assert len(groups[0]) == 5

    def test_preserves_order(self):
        trades = [
            {"timestamp": 1704067200000, "id": 1},
            {"timestamp": 1704067201000, "id": 2},
            {"timestamp": 1704153600000, "id": 3},
        ]
        groups = split_trades_by_day(trades)
        assert groups[0][0]["id"] == 1
        assert groups[0][1]["id"] == 2
        assert groups[1][0]["id"] == 3


class TestPreWriteGate:
    """Test the day-mode pre-write gate in bulk_operations.py."""

    def test_polars_cross_midnight_rejected(self):
        """Polars DataFrame with a cross-midnight bar raises ValueError."""

        import polars as pl

        df = pl.DataFrame({
            "open_time_ms": [1704150000000],   # 2024-01-01 23:00
            "close_time_ms": [1704160000000],  # 2024-01-02 01:46
        })

        from opendeviationbar.validation.day_mode.guards import DAY_MS

        # Simulate gate logic directly (avoids needing full cache setup)
        violations = df.filter(
            (pl.col("open_time_ms") > 0)
            & (pl.col("close_time_ms") > 0)
            & ((pl.col("open_time_ms") // DAY_MS) != (pl.col("close_time_ms") // DAY_MS))
        )
        assert len(violations) == 1

    def test_polars_same_day_passes(self):
        """Polars DataFrame with same-day bar passes gate."""
        import polars as pl

        df = pl.DataFrame({
            "open_time_ms": [1704067200000],   # 2024-01-01 00:00
            "close_time_ms": [1704110400000],  # 2024-01-01 12:00
        })

        from opendeviationbar.validation.day_mode.guards import DAY_MS

        violations = df.filter(
            (pl.col("open_time_ms") > 0)
            & (pl.col("close_time_ms") > 0)
            & ((pl.col("open_time_ms") // DAY_MS) != (pl.col("close_time_ms") // DAY_MS))
        )
        assert len(violations) == 0

    def test_zero_timestamp_not_false_positive(self):
        """Zero timestamps should NOT trigger cross-midnight rejection."""
        import polars as pl

        df = pl.DataFrame({
            "open_time_ms": [0],
            "close_time_ms": [1704110400000],
        })

        from opendeviationbar.validation.day_mode.guards import DAY_MS

        # With the > 0 guard, zero-timestamp rows are skipped
        violations = df.filter(
            (pl.col("open_time_ms") > 0)
            & (pl.col("close_time_ms") > 0)
            & ((pl.col("open_time_ms") // DAY_MS) != (pl.col("close_time_ms") // DAY_MS))
        )
        assert len(violations) == 0

    def test_pandas_cross_midnight_rejected(self):
        """Pandas DataFrame with a cross-midnight bar triggers rejection."""
        import pandas as pd

        df = pd.DataFrame({
            "open_time_ms": [1704150000000],
            "close_time_ms": [1704160000000],
        })

        from opendeviationbar.validation.day_mode.guards import DAY_MS

        valid = df[(df["open_time_ms"] > 0) & (df["close_time_ms"] > 0)]
        cross = valid[(valid["open_time_ms"] // DAY_MS) != (valid["close_time_ms"] // DAY_MS)]
        assert len(cross) == 1

    def test_pandas_zero_timestamp_skipped(self):
        """Pandas zero-timestamp rows should not trigger false positive."""
        import pandas as pd

        df = pd.DataFrame({
            "open_time_ms": [0],
            "close_time_ms": [1704110400000],
        })

        from opendeviationbar.validation.day_mode.guards import DAY_MS

        valid = df[(df["open_time_ms"] > 0) & (df["close_time_ms"] > 0)]
        cross = valid[(valid["open_time_ms"] // DAY_MS) != (valid["close_time_ms"] // DAY_MS)]
        assert len(cross) == 0


class TestSidecarCrossMidnightFilter:
    """Test the L1 sidecar cross-midnight bar filter logic."""

    def test_cross_midnight_bar_detected(self):
        """A bar spanning midnight should be detected by the filter."""
        bar = {
            "open_time_ms": 1704150000000,   # 2024-01-01 23:00 UTC
            "close_time_ms": 1704160000000,  # 2024-01-02 01:46 UTC
        }
        open_ms = bar["open_time_ms"]
        close_ms = bar["close_time_ms"]
        assert (open_ms // 86_400_000) != (close_ms // 86_400_000)

    def test_same_day_bar_passes(self):
        """A bar within the same UTC day should pass the filter."""
        bar = {
            "open_time_ms": 1704067200000,   # 2024-01-01 00:00 UTC
            "close_time_ms": 1704110400000,  # 2024-01-01 12:00 UTC
        }
        open_ms = bar["open_time_ms"]
        close_ms = bar["close_time_ms"]
        assert (open_ms // 86_400_000) == (close_ms // 86_400_000)

    def test_zero_timestamp_passes(self):
        """Bars with zero timestamps should not be filtered."""
        bar = {"open_time_ms": 0, "close_time_ms": 1704110400000}
        open_ms = bar["open_time_ms"]
        close_ms = bar["close_time_ms"]
        # The guard checks open_ms > 0, so this bar is NOT filtered
        is_cross = open_ms > 0 and close_ms > 0 and (open_ms // 86_400_000) != (close_ms // 86_400_000)
        assert not is_cross

    def test_midnight_boundary_bar_same_day(self):
        """A bar closing exactly at midnight belongs to the opening day."""
        bar = {
            "open_time_ms": 1704150000000,   # 2024-01-01 23:00 UTC
            "close_time_ms": 1704153600000,  # 2024-01-02 00:00 UTC (midnight)
        }
        open_ms = bar["open_time_ms"]
        close_ms = bar["close_time_ms"]
        # Midnight exactly = day boundary, open_day != close_day → filtered
        assert (open_ms // 86_400_000) != (close_ms // 86_400_000)


class TestStatheraNoExclusions:
    """Stathera invariant: all tid_delta > 0 are real gaps, no exclusions."""

    def test_no_day_boundary_exclusion(self):
        """day_boundary_exclusion has been removed — no gaps are hidden."""
        import opendeviationbar.clickhouse.stathera_query as sq

        assert not hasattr(sq, "day_boundary_exclusion")

    def test_no_day_diff_expr(self):
        """DAY_DIFF_EXPR has been removed — no time-based gap classification."""
        import opendeviationbar.clickhouse.stathera_query as sq

        assert not hasattr(sq, "DAY_DIFF_EXPR")
