#!/usr/bin/env python3
"""Test ouroboros module for cyclical reset boundaries.

Tests cover:
- Aion mode (continuous, no resets)
- Segment iteration
- Exchange session detection
- Rejection of legacy modes (year, month, week, day)
"""

from datetime import UTC, date, datetime

import pytest
from opendeviationbar.ouroboros import (
    ExchangeSessionFlags,
    OrphanedBarMetadata,
    OuroborosBoundary,
    OuroborosMode,
    get_active_exchange_sessions,
    get_ouroboros_boundaries,
    iter_ouroboros_segments,
    validate_ouroboros_mode,
)


class TestOuroborosMode:
    """Test OuroborosMode enum."""

    def test_aion_mode_value(self):
        """Test AION enum value is correct."""
        assert OuroborosMode.AION.value == "aion"

    def test_aion_mode_construction(self):
        """Test OuroborosMode('aion') returns AION variant."""
        assert OuroborosMode("aion") == OuroborosMode.AION

    def test_legacy_modes_removed(self):
        """Test that legacy modes (year, month, week, day) are removed."""
        assert not hasattr(OuroborosMode, "YEAR")
        assert not hasattr(OuroborosMode, "MONTH")
        assert not hasattr(OuroborosMode, "WEEK")
        assert not hasattr(OuroborosMode, "DAY")


class TestOuroborosBoundary:
    """Test OuroborosBoundary dataclass."""

    def test_boundary_creation(self):
        """Test creating a boundary."""
        ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        boundary = OuroborosBoundary(
            timestamp=ts,
            mode=OuroborosMode.AION,
            reason="test_boundary",
        )
        assert boundary.timestamp == ts
        assert boundary.mode == OuroborosMode.AION
        assert boundary.reason == "test_boundary"

    def test_timestamp_ms(self):
        """Test timestamp_ms property."""
        ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        boundary = OuroborosBoundary(
            timestamp=ts,
            mode=OuroborosMode.AION,
            reason="test_boundary",
        )
        # 2024-01-01 00:00:00 UTC in milliseconds
        expected_ms = 1704067200000
        assert boundary.timestamp_ms == expected_ms

    def test_timestamp_us(self):
        """Test timestamp_us property."""
        ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        boundary = OuroborosBoundary(
            timestamp=ts,
            mode=OuroborosMode.AION,
            reason="test_boundary",
        )
        expected_us = 1704067200000000
        assert boundary.timestamp_us == expected_us

    def test_boundary_is_frozen(self):
        """Test boundary is immutable."""
        ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        boundary = OuroborosBoundary(
            timestamp=ts,
            mode=OuroborosMode.AION,
            reason="test_boundary",
        )
        with pytest.raises(AttributeError):
            boundary.timestamp = datetime(2025, 1, 1, 0, 0, 0, tzinfo=UTC)


class TestOrphanedBarMetadata:
    """Test OrphanedBarMetadata dataclass."""

    def test_default_values(self):
        """Test default values."""
        metadata = OrphanedBarMetadata()
        assert metadata.is_orphan is True
        assert metadata.ouroboros_boundary is None
        assert metadata.reason is None
        assert metadata.expected_duration_us is None

    def test_full_metadata(self):
        """Test with all fields populated."""
        boundary_ts = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        metadata = OrphanedBarMetadata(
            is_orphan=True,
            ouroboros_boundary=boundary_ts,
            reason="aion_boundary",
            expected_duration_us=3600000000,  # 1 hour
        )
        assert metadata.ouroboros_boundary == boundary_ts
        assert metadata.reason == "aion_boundary"
        assert metadata.expected_duration_us == 3600000000


class TestGetOuroborosBoundaries:
    """Test get_ouroboros_boundaries function."""

    def test_aion_boundaries_default_mode(self):
        """Test that default mode is 'aion' (no boundaries)."""
        boundaries = get_ouroboros_boundaries(date(2024, 3, 1), date(2024, 3, 3))
        assert len(boundaries) == 0

    def test_aion_mode_returns_empty(self):
        """Test that Aion mode returns no boundaries (continuous, no resets)."""
        boundaries = get_ouroboros_boundaries(date(2024, 1, 1), date(2024, 12, 31), "aion")
        assert boundaries == []

    def test_legacy_mode_year_rejected(self):
        """Test that 'year' mode raises ValueError."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_ouroboros_boundaries(date(2024, 1, 1), date(2024, 12, 31), "year")

    def test_legacy_mode_month_rejected(self):
        """Test that 'month' mode raises ValueError."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_ouroboros_boundaries(date(2024, 1, 1), date(2024, 3, 31), "month")

    def test_legacy_mode_week_rejected(self):
        """Test that 'week' mode raises ValueError."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_ouroboros_boundaries(date(2024, 1, 1), date(2024, 1, 31), "week")

    def test_legacy_mode_day_rejected(self):
        """Test that 'day' mode raises ValueError (day mode deprecated)."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_ouroboros_boundaries(date(2024, 1, 1), date(2024, 1, 31), "day")


class TestIterOuroborosSegments:
    """Test iter_ouroboros_segments function."""

    def test_aion_mode_single_segment(self):
        """Test Aion mode yields single segment spanning full range."""
        segments = list(
            iter_ouroboros_segments(date(2024, 1, 1), date(2024, 1, 31), "aion")
        )
        assert len(segments) == 1
        start, end, boundary = segments[0]
        assert start == datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        assert end == datetime(2024, 1, 31, 23, 59, 59, 999999, tzinfo=UTC)
        assert boundary is None

    def test_segments_is_generator(self):
        """Test segment iteration returns generator."""
        segments_gen = iter_ouroboros_segments(
            date(2024, 1, 1), date(2024, 1, 3), "aion"
        )
        assert hasattr(segments_gen, "__iter__")
        assert hasattr(segments_gen, "__next__")


class TestExchangeSessionFlags:
    """Test ExchangeSessionFlags dataclass."""

    def test_flags_creation(self):
        """Test creating session flags."""
        flags = ExchangeSessionFlags(
            sydney=True, tokyo=True, london=False, newyork=False
        )
        assert flags.sydney is True
        assert flags.tokyo is True
        assert flags.london is False
        assert flags.newyork is False

    def test_to_dict(self):
        """Test converting to dict with column names."""
        flags = ExchangeSessionFlags(
            sydney=True, tokyo=False, london=True, newyork=False
        )
        d = flags.to_dict()
        assert d == {
            "exchange_session_sydney": True,
            "exchange_session_tokyo": False,
            "exchange_session_london": True,
            "exchange_session_newyork": False,
        }

    def test_flags_is_frozen(self):
        """Test flags are immutable."""
        flags = ExchangeSessionFlags(
            sydney=True, tokyo=True, london=False, newyork=False
        )
        with pytest.raises(AttributeError):
            flags.sydney = False


class TestGetActiveExchangeSessions:
    """Test get_active_exchange_sessions function."""

    def test_tokyo_session_morning(self):
        """Test Tokyo session during Tokyo morning (9 AM JST = 0 AM UTC)."""
        ts = datetime(2024, 1, 15, 0, 0, 0, tzinfo=UTC)  # Monday
        flags = get_active_exchange_sessions(ts)
        assert flags.tokyo is True

    def test_london_session_morning(self):
        """Test London session during London morning (9 AM GMT = 9 AM UTC)."""
        ts = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)  # Monday
        flags = get_active_exchange_sessions(ts)
        assert flags.london is True

    def test_newyork_session_afternoon(self):
        """Test New York session (2 PM EST = 7 PM UTC)."""
        ts = datetime(2024, 1, 15, 19, 0, 0, tzinfo=UTC)  # Monday
        flags = get_active_exchange_sessions(ts)
        assert flags.newyork is True

    def test_no_sessions_weekend(self):
        """Test no sessions active on weekend."""
        ts = datetime(2024, 1, 13, 12, 0, 0, tzinfo=UTC)  # Saturday
        flags = get_active_exchange_sessions(ts)
        assert flags.sydney is False
        assert flags.tokyo is False
        assert flags.london is False
        assert flags.newyork is False

    def test_session_overlap_london_newyork(self):
        """Test London/New York overlap (afternoon UTC)."""
        ts = datetime(2024, 1, 15, 15, 30, 0, tzinfo=UTC)  # Monday
        flags = get_active_exchange_sessions(ts)
        assert flags.london is True
        assert flags.newyork is True

    def test_sydney_session(self):
        """Test Sydney session (Sydney mid-morning = late UTC previous day)."""
        ts = datetime(2024, 1, 16, 1, 0, 0, tzinfo=UTC)  # Tuesday UTC
        flags = get_active_exchange_sessions(ts)
        assert flags.sydney is True


class TestValidateOuroborosMode:
    """Test validate_ouroboros_mode function."""

    def test_valid_aion_mode(self):
        """Test 'aion' is accepted."""
        assert validate_ouroboros_mode("aion") == "aion"

    def test_day_mode_rejected(self):
        """Test 'day' is rejected (day mode deprecated)."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            validate_ouroboros_mode("day")

    def test_legacy_modes_rejected(self):
        """Test legacy modes raise ValueError."""
        for mode in ("year", "month", "week"):
            with pytest.raises(ValueError, match="Invalid ouroboros mode"):
                validate_ouroboros_mode(mode)

    def test_invalid_mode(self):
        """Test invalid mode raises ValueError."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            validate_ouroboros_mode("hourly")

    def test_invalid_mode_none(self):
        """Test 'none' is not a valid mode."""
        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            validate_ouroboros_mode("none")


class TestEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_timestamp_ms_precision(self):
        """Test timestamp_ms has millisecond precision."""
        ts = datetime(2024, 1, 1, 12, 30, 45, 123456, tzinfo=UTC)
        boundary = OuroborosBoundary(
            timestamp=ts,
            mode=OuroborosMode.AION,
            reason="test",
        )
        expected_ms = int(ts.timestamp() * 1000)
        assert boundary.timestamp_ms == expected_ms

    def test_timestamp_us_precision(self):
        """Test timestamp_us has microsecond precision."""
        ts = datetime(2024, 1, 1, 12, 30, 45, 123456, tzinfo=UTC)
        boundary = OuroborosBoundary(
            timestamp=ts,
            mode=OuroborosMode.AION,
            reason="test",
        )
        expected_us = int(ts.timestamp() * 1_000_000)
        assert boundary.timestamp_us == expected_us


class TestOuroborosAPIContract:
    """Fast contract tests for ouroboros API signatures and validation."""

    def test_default_ouroboros_is_none(self):
        """Default ouroboros_mode parameter is None (resolved from config at runtime)."""
        import inspect

        from opendeviationbar import get_open_deviation_bars

        sig = inspect.signature(get_open_deviation_bars)
        ouroboros_param = sig.parameters["ouroboros_mode"]
        assert ouroboros_param.default is None

    def test_ouroboros_none_is_rejected(self):
        """ouroboros='none' (string) is rejected."""
        from opendeviationbar import get_open_deviation_bars

        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_open_deviation_bars(
                "BTCUSDT",
                "2024-01-01",
                "2024-01-07",
                threshold_decimal_bps=250,
                ouroboros_mode="none",
            )

    def test_ouroboros_invalid_value_rejected(self):
        """Invalid ouroboros values are rejected."""
        from opendeviationbar import get_open_deviation_bars

        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_open_deviation_bars(
                "BTCUSDT",
                "2024-01-01",
                "2024-01-07",
                threshold_decimal_bps=250,
                ouroboros_mode="daily",
            )

    def test_ouroboros_day_mode_rejected(self):
        """Day mode is rejected (deprecated)."""
        from opendeviationbar import get_open_deviation_bars

        with pytest.raises(ValueError, match="Invalid ouroboros mode"):
            get_open_deviation_bars(
                "BTCUSDT",
                "2024-01-01",
                "2024-01-07",
                threshold_decimal_bps=250,
                ouroboros_mode="aion",
            )
