"""Unit tests for _compute_quality_score in rr_scanner_service."""

import pytest

from app.services.rr_scanner_service import _compute_quality_score


# ---------------------------------------------------------------------------
# 4.1 — Known inputs with hand-computed expected outputs
# ---------------------------------------------------------------------------

class TestKnownInputs:
    def test_typical_candidate(self):
        # rr=5, strength=80, distance=3, entry_price=100
        # norm_rr = 5/10 = 0.5
        # norm_strength = 80/100 = 0.8
        # norm_proximity = 1 - 3/100 = 0.97
        # score = 0.35*0.5 + 0.35*0.8 + 0.30*0.97 = 0.175 + 0.28 + 0.291 = 0.746
        result = _compute_quality_score(rr=5.0, strength=80, distance=3.0, entry_price=100.0)
        assert result == pytest.approx(0.746, abs=1e-9)

    def test_weak_distant_candidate(self):
        # rr=8, strength=10, distance=50, entry_price=100
        # norm_rr = 8/10 = 0.8
        # norm_strength = 10/100 = 0.1
        # norm_proximity = 1 - 50/100 = 0.5
        # score = 0.35*0.8 + 0.35*0.1 + 0.30*0.5 = 0.28 + 0.035 + 0.15 = 0.465
        result = _compute_quality_score(rr=8.0, strength=10, distance=50.0, entry_price=100.0)
        assert result == pytest.approx(0.465, abs=1e-9)

    def test_strong_near_candidate(self):
        # rr=2, strength=95, distance=5, entry_price=200
        # norm_rr = 2/10 = 0.2
        # norm_strength = 95/100 = 0.95
        # norm_proximity = 1 - 5/200 = 0.975
        # score = 0.35*0.2 + 0.35*0.95 + 0.30*0.975 = 0.07 + 0.3325 + 0.2925 = 0.695
        result = _compute_quality_score(rr=2.0, strength=95, distance=5.0, entry_price=200.0)
        assert result == pytest.approx(0.695, abs=1e-9)

    def test_custom_weights(self):
        # rr=4, strength=50, distance=10, entry_price=100
        # norm_rr = 4/10 = 0.4, norm_strength = 0.5, norm_proximity = 1 - 10/100 = 0.9
        # With w_rr=0.5, w_strength=0.3, w_proximity=0.2:
        # score = 0.5*0.4 + 0.3*0.5 + 0.2*0.9 = 0.2 + 0.15 + 0.18 = 0.53
        result = _compute_quality_score(
            rr=4.0, strength=50, distance=10.0, entry_price=100.0,
            w_rr=0.5, w_strength=0.3, w_proximity=0.2,
        )
        assert result == pytest.approx(0.53, abs=1e-9)

    def test_custom_rr_cap(self):
        # rr=3, strength=60, distance=8, entry_price=100, rr_cap=5
        # norm_rr = 3/5 = 0.6
        # norm_strength = 60/100 = 0.6
        # norm_proximity = 1 - 8/100 = 0.92
        # score = 0.35*0.6 + 0.35*0.6 + 0.30*0.92 = 0.21 + 0.21 + 0.276 = 0.696
        result = _compute_quality_score(
            rr=3.0, strength=60, distance=8.0, entry_price=100.0, rr_cap=5.0,
        )
        assert result == pytest.approx(0.696, abs=1e-9)


# ---------------------------------------------------------------------------
# 4.2 — Edge cases
# ---------------------------------------------------------------------------

class TestEdgeCases:
    def test_strength_zero(self):
        # strength=0 → norm_strength=0
        # rr=5, distance=10, entry_price=100
        # norm_rr=0.5, norm_strength=0.0, norm_proximity=0.9
        # score = 0.35*0.5 + 0.35*0.0 + 0.30*0.9 = 0.175 + 0.0 + 0.27 = 0.445
        result = _compute_quality_score(rr=5.0, strength=0, distance=10.0, entry_price=100.0)
        assert result == pytest.approx(0.445, abs=1e-9)

    def test_strength_100(self):
        # strength=100 → norm_strength=1.0
        # rr=5, distance=10, entry_price=100
        # norm_rr=0.5, norm_strength=1.0, norm_proximity=0.9
        # score = 0.35*0.5 + 0.35*1.0 + 0.30*0.9 = 0.175 + 0.35 + 0.27 = 0.795
        result = _compute_quality_score(rr=5.0, strength=100, distance=10.0, entry_price=100.0)
        assert result == pytest.approx(0.795, abs=1e-9)

    def test_distance_zero(self):
        # distance=0 → norm_proximity = 1 - 0/100 = 1.0
        # rr=5, strength=50, entry_price=100
        # norm_rr=0.5, norm_strength=0.5, norm_proximity=1.0
        # score = 0.35*0.5 + 0.35*0.5 + 0.30*1.0 = 0.175 + 0.175 + 0.3 = 0.65
        result = _compute_quality_score(rr=5.0, strength=50, distance=0.0, entry_price=100.0)
        assert result == pytest.approx(0.65, abs=1e-9)

    def test_rr_at_cap(self):
        # rr=10 (== rr_cap) → norm_rr = min(10/10, 1.0) = 1.0
        # strength=50, distance=10, entry_price=100
        # norm_strength=0.5, norm_proximity=0.9
        # score = 0.35*1.0 + 0.35*0.5 + 0.30*0.9 = 0.35 + 0.175 + 0.27 = 0.795
        result = _compute_quality_score(rr=10.0, strength=50, distance=10.0, entry_price=100.0)
        assert result == pytest.approx(0.795, abs=1e-9)

    def test_rr_above_cap(self):
        # rr=15 (> rr_cap=10) → norm_rr = min(15/10, 1.0) = 1.0 (capped)
        # Same result as rr_at_cap since norm_rr is capped at 1.0
        result = _compute_quality_score(rr=15.0, strength=50, distance=10.0, entry_price=100.0)
        assert result == pytest.approx(0.795, abs=1e-9)

    def test_rr_above_cap_equals_rr_at_cap(self):
        # Explicitly verify capping: rr=15 and rr=10 produce the same score
        at_cap = _compute_quality_score(rr=10.0, strength=50, distance=10.0, entry_price=100.0)
        above_cap = _compute_quality_score(rr=15.0, strength=50, distance=10.0, entry_price=100.0)
        assert at_cap == pytest.approx(above_cap, abs=1e-9)


# ---------------------------------------------------------------------------
# 4.3 — Normalized components stay in 0–1 range
# ---------------------------------------------------------------------------

class TestNormalizedComponentsRange:
    """Verify that each normalized component stays within [0, 1]."""

    @pytest.mark.parametrize("rr, rr_cap", [
        (0.0, 10.0),
        (5.0, 10.0),
        (10.0, 10.0),
        (15.0, 10.0),
        (100.0, 10.0),
        (3.0, 5.0),
        (7.0, 5.0),
    ])
    def test_norm_rr_in_range(self, rr, rr_cap):
        norm_rr = min(rr / rr_cap, 1.0)
        assert 0.0 <= norm_rr <= 1.0

    @pytest.mark.parametrize("strength", [0, 1, 50, 99, 100])
    def test_norm_strength_in_range(self, strength):
        norm_strength = strength / 100.0
        assert 0.0 <= norm_strength <= 1.0

    @pytest.mark.parametrize("distance, entry_price", [
        (0.0, 100.0),
        (1.0, 100.0),
        (50.0, 100.0),
        (100.0, 100.0),
        (200.0, 100.0),  # distance > entry_price → clamped
        (0.5, 50.0),
    ])
    def test_norm_proximity_in_range(self, distance, entry_price):
        norm_proximity = 1.0 - min(distance / entry_price, 1.0)
        assert 0.0 <= norm_proximity <= 1.0

    @pytest.mark.parametrize("rr, strength, distance, entry_price", [
        (0.0, 0, 0.0, 100.0),       # all minimums
        (10.0, 100, 0.0, 100.0),     # all maximums
        (15.0, 100, 200.0, 100.0),   # rr above cap, distance > entry
        (5.0, 50, 50.0, 100.0),      # mid-range values
        (1.0, 1, 99.0, 100.0),       # near-minimum non-zero
    ])
    def test_total_score_in_zero_one(self, rr, strength, distance, entry_price):
        score = _compute_quality_score(rr=rr, strength=strength, distance=distance, entry_price=entry_price)
        assert 0.0 <= score <= 1.0
