"""Unit tests for detect_sr_levels and related pure helpers."""

from __future__ import annotations

from app.services.sr_service import (
    MAX_LEVELS,
    _bar_respect_weight,
    _cap_levels,
    _gate_target_range_centers,
    _merge_levels,
    _round_number_candidates,
    _strength_from_respects,
    detect_gate_target_ladder,
    detect_sr_levels,
)


def _make_series(
    n: int = 300,
    *,
    base: float = 100.0,
    support: float = 95.0,
    resistance: float = 110.0,
) -> tuple[list[float], list[float], list[float], list[int]]:
    """Synthetic OHLCV that repeatedly tests support/resistance."""
    highs: list[float] = []
    lows: list[float] = []
    closes: list[float] = []
    volumes: list[int] = []

    price = base
    for i in range(n):
        phase = i % 40
        if phase < 15:
            # Drift down toward support, bounce
            target = support
            price = price + (target - price) * 0.25
            low = min(price, support) - 0.3
            high = price + 1.0
            close = max(price, support + 0.5) if phase > 12 else price
        elif phase < 30:
            # Drift up toward resistance, reject
            target = resistance
            price = price + (target - price) * 0.25
            high = max(price, resistance) + 0.3
            low = price - 1.0
            close = min(price, resistance - 0.5) if phase > 27 else price
        else:
            price = base + (i % 7) * 0.2
            high = price + 1.0
            low = price - 1.0
            close = price

        # Occasional clear swing extremes
        if i % 55 == 25:
            high = resistance + 1.0
            close = resistance - 1.0
            low = close - 1.0
        if i % 55 == 50:
            low = support - 1.0
            close = support + 1.0
            high = close + 1.0

        highs.append(high)
        lows.append(low)
        closes.append(close)
        volumes.append(1000 + (i % 10) * 50)
        price = close

    return highs, lows, closes, volumes


class TestBarRespectWeight:
    def test_no_interaction(self):
        assert _bar_respect_weight(100.0, 90.0, 85.0, 88.0, 87.0, 0.005) == 0.0

    def test_support_rejection(self):
        # Low probes at 100, closes above with recovery wick
        w = _bar_respect_weight(100.0, 103.0, 99.8, 102.0, 101.0, 0.005)
        assert w >= 0.9

    def test_resistance_rejection(self):
        # High probes at 100, closes below
        w = _bar_respect_weight(100.0, 100.2, 97.0, 98.0, 99.0, 0.005)
        assert w >= 0.9

    def test_pass_through_lower_weight(self):
        # Prev below, close above, bar spans through without probing extremes at level
        w = _bar_respect_weight(100.0, 105.0, 95.0, 104.0, 96.0, 0.005)
        assert w < 0.5


class TestStrengthFromRespects:
    def test_pass_through_not_maximal(self):
        """Central pass-through levels should not pin at strength 100."""
        n = 200
        # Trending series that passes through 100 many times
        closes = [80.0 + i * 0.25 for i in range(n)]
        highs = [c + 1.0 for c in closes]
        lows = [c - 1.0 for c in closes]
        strength = _strength_from_respects(100.0, highs, lows, closes, 0.005)
        assert strength < 100

    def test_repeated_rejection_stronger_than_no_touch(self):
        n = 120
        level = 100.0
        # Bars that repeatedly probe support (low near level) and close above
        highs = [103.0] * n
        lows = [99.8] * n
        closes = [102.0] * n
        strong = _strength_from_respects(level, highs, lows, closes, 0.01, base=10)

        far_highs = [120.0] * n
        far_lows = [118.0] * n
        far_closes = [119.0] * n
        weak = _strength_from_respects(level, far_highs, far_lows, far_closes, 0.01, base=10)
        assert strong > weak


class TestRoundNumbers:
    def test_near_spot(self):
        levels = _round_number_candidates(103.0)
        assert levels
        assert all(abs(p - 103.0) / 103.0 <= 0.15 + 1e-9 for p in levels)
        assert len(levels) <= 8

    def test_non_positive_price(self):
        assert _round_number_candidates(0.0) == []
        assert _round_number_candidates(-5.0) == []


class TestCapLevels:
    def test_interleaves_sides(self):
        levels = [
            {"price_level": 90.0, "type": "support", "strength": 80, "detection_method": "x"},
            {"price_level": 91.0, "type": "support", "strength": 70, "detection_method": "x"},
            {"price_level": 92.0, "type": "support", "strength": 60, "detection_method": "x"},
            {"price_level": 110.0, "type": "resistance", "strength": 50, "detection_method": "x"},
            {"price_level": 111.0, "type": "resistance", "strength": 40, "detection_method": "x"},
        ]
        capped = _cap_levels(levels, max_levels=4)
        assert len(capped) == 4
        types = {lvl["type"] for lvl in capped}
        assert "support" in types
        assert "resistance" in types


class TestLevelEvidence:
    def test_merge_preserves_sources_and_rejection_evidence(self):
        levels = [
            {
                "price_level": 100.0,
                "type": "",
                "strength": 55,
                "detection_method": "pivot_point",
                "sources": ["pivot_point"],
                "rejection_count": 3,
                "last_rejection_age": 12,
                "weighted_respects": 1.5,
            },
            {
                "price_level": 100.3,
                "type": "",
                "strength": 40,
                "detection_method": "round_number",
                "sources": ["round_number"],
                "rejection_count": 1,
                "last_rejection_age": 4,
                "weighted_respects": 0.5,
            },
        ]
        merged = _merge_levels(levels, tolerance=0.005)
        assert len(merged) == 1
        assert set(merged[0]["sources"]) == {"pivot_point", "round_number"}
        assert merged[0]["rejection_count"] == 3
        assert merged[0]["last_rejection_age"] == 4


class TestDetectSrLevels:
    def test_returns_capped_tagged_levels(self):
        highs, lows, closes, volumes = _make_series()
        levels = detect_sr_levels(highs, lows, closes, volumes)
        assert levels
        assert len(levels) <= MAX_LEVELS
        for lvl in levels:
            assert lvl["type"] in ("support", "resistance")
            assert 0 <= lvl["strength"] <= 100
            assert lvl["detection_method"] in (
                "volume_profile",
                "pivot_point",
                "merged",
                "round_number",
            )
            assert lvl["price_level"] > 0
            assert lvl["sources"]
            assert lvl["rejection_count"] >= 0
        # Sorted by strength desc
        strengths = [lvl["strength"] for lvl in levels]
        assert strengths == sorted(strengths, reverse=True)

    def test_far_fewer_than_old_grid(self):
        """Should not produce a near-1%-spacing grid of ~70 levels."""
        highs, lows, closes, volumes = _make_series(n=500)
        levels = detect_sr_levels(highs, lows, closes, volumes)
        assert len(levels) <= MAX_LEVELS

    def test_empty_input(self):
        assert detect_sr_levels([], [], [], []) == []

    def test_explicit_tolerance(self):
        highs, lows, closes, volumes = _make_series()
        tight = detect_sr_levels(highs, lows, closes, volumes, tolerance=0.001)
        wide = detect_sr_levels(highs, lows, closes, volumes, tolerance=0.05)
        # Wider merge should not produce more levels
        assert len(wide) <= len(tight) + 2  # allow small jitter from scoring

    def test_levels_near_structural_areas(self):
        """At least some levels should land near the synthetic S/R band."""
        highs, lows, closes, volumes = _make_series(
            n=400, support=95.0, resistance=110.0
        )
        levels = detect_sr_levels(highs, lows, closes, volumes)
        prices = [lvl["price_level"] for lvl in levels]
        near_support = any(abs(p - 95.0) / 95.0 < 0.05 for p in prices)
        near_resist = any(abs(p - 110.0) / 110.0 < 0.05 for p in prices)
        # Round numbers / VP may dominate; require at least one structural band hit
        assert near_support or near_resist or any(
            abs(p - 100.0) / 100.0 < 0.08 for p in prices
        )

    def test_strength_not_all_pinned_at_100(self):
        highs, lows, closes, volumes = _make_series(n=400)
        levels = detect_sr_levels(highs, lows, closes, volumes)
        if len(levels) >= 3:
            pinned = sum(1 for lvl in levels if lvl["strength"] == 100)
            assert pinned < len(levels)

    def test_gate_target_range_centers_cover_the_observed_range(self):
        highs, lows, closes, _ = _make_series(n=500)
        centers = _gate_target_range_centers(highs, lows, closes)

        assert len(centers) == 20
        assert centers == sorted(centers)
        assert min(lows) < centers[0] < centers[-1] < max(highs)

    def test_gate_target_ladder_is_dense_transient_price_traffic(self):
        highs, lows, closes, _ = _make_series(n=500)
        levels = detect_gate_target_ladder(highs, lows, closes)

        assert len(levels) > MAX_LEVELS
        assert any("range_grid" in level["sources"] for level in levels)
        assert all("volume_profile" not in level["sources"] for level in levels)
        assert all(0 <= level["strength"] <= 100 for level in levels)
        assert all(level["rejection_count"] >= 0 for level in levels)

    def test_gate_target_ladder_is_deterministic(self):
        highs, lows, closes, _ = _make_series(n=500)
        assert detect_gate_target_ladder(highs, lows, closes) == (
            detect_gate_target_ladder(list(highs), list(lows), list(closes))
        )
