"""
test_direction_unanimity_bugfix.py

Regression test for the "blended threshold lets one bad leg get outvoted"
bug found in production logs on 2026-09-14. fast_prediction_engine.py's
_direction_and_confidence() used to average four weighted scores
(momentum 0.30, volume 0.25, vwap 0.25, ema9 0.20) and call it UP once
the blend cleared 0.55 -- so three strong legs could carry a UP direction
straight through one actively-contradicting leg:

  - ALLT entered with momentum=Strong, vwap=Bullish, ema9=Bullish, but
    volume=Declining (rvol=1.18, but decelerating, accel=0.628) -- the
    blend still cleared 0.775 (0.659 after the EXTENDED-extension
    penalty), comfortably above the 0.55 UP threshold. It faded within
    6 seconds of fill.
  - TNON entered with momentum labeled "Building" -- which fires
    whenever momentum_acceleration > 0 even with slope_5m <= 0 (price
    still falling, just decelerating). Its real reading was
    slope_5m=-0.157: not actually rising at all.
  - Multiple symbols cleared UP with resistance=CAUTION (closer to
    the overhead level than the "safe" distance, but not quite
    TOO_CLOSE) -- only a 0.95x confidence penalty, never a block.

Per explicit instruction ("we don't have lots of indicators so we need
to make sure that they are all positive before an entry"), direction=UP
requires unanimous agreement across momentum (slope_5m > 0, i.e. Strong
or Weakening -- NOT Building), volume (Expanding or Steady -- NOT
Declining), and resistance=SAFE (genuinely clear, or a confirmed
breakout in progress). confidence stays the same blended score over all
four inputs, purely for shortlist ranking/logging, no longer driving
direction.

[ADJUSTED same day, 2026-09-14] volume was first tried as Expanding-only,
then loosened to "not Declining" after a full-day retrospective replay
showed Expanding-only collapsed 48 real trades to 1. vwap and ema9 were
then dropped from the hard gate entirely (explicit instruction, "let's
disable vwap and ema9 for tomorrow run") after the same replay
methodology showed the 5-condition version only recovered 2 of 48 real
trades (net +$0.85) vs. 3 of 48 (net +$5.78) with vwap/ema9 excluded --
they're still fully reflected in the confidence blend used for shortlist
ranking, just no longer required to gate entry outright.

Run:
    python -m pytest tests/test_direction_unanimity_bugfix.py -v
or:
    python tests/test_direction_unanimity_bugfix.py
"""

import sys
import os
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from fast_prediction_engine import (  # noqa: E402
    _direction_and_confidence, DEFAULT_CONFIG, DIRECTION_UP, DIRECTION_NEUTRAL,
    RESISTANCE_SAFE, RESISTANCE_CAUTION, EXTENSION_NORMAL, EXTENSION_EXTENDED,
)


class TestDirectionUnanimityBugfix(unittest.TestCase):
    def test_all_positive_still_fires_up(self):
        """Sanity check: a genuinely clean setup must still qualify."""
        direction, confidence = _direction_and_confidence(
            "Strong", "Expanding", "Bullish", "Bullish", EXTENSION_NORMAL, RESISTANCE_SAFE, DEFAULT_CONFIG)
        self.assertEqual(direction, DIRECTION_UP)

    def test_declining_volume_blocks_up_despite_strong_momentum(self):
        """Reproduces ALLT's 09:53:21 entry inputs exactly: Strong
        momentum, Bullish vwap/ema9, but Declining volume. Old blend
        (0.775, or 0.659 after the EXTENDED penalty) cleared the 0.55
        threshold; the new unanimity rule must not."""
        direction, confidence = _direction_and_confidence(
            "Strong", "Declining", "Bullish", "Bullish", EXTENSION_EXTENDED, RESISTANCE_SAFE, DEFAULT_CONFIG)
        self.assertNotEqual(direction, DIRECTION_UP)
        # confidence is still computed (ranking/logging use only) --
        # this is the exact 65.9 the real log line recorded.
        self.assertAlmostEqual(confidence, 65.9, delta=0.1)

    def test_steady_volume_now_counts_as_positive(self):
        """[ADJUSTED 2026-09-14] Requiring volume=Expanding specifically
        was retrospectively replayed against every real entry opportunity
        today and collapsed 48 real trades to 1 (Expanding only occurred
        on 9.3% of ticks). Loosened to "not Declining" -- Steady must now
        pass, while Declining (tested above) still must not."""
        direction, confidence = _direction_and_confidence(
            "Strong", "Steady", "Bullish", "Bullish", EXTENSION_NORMAL, RESISTANCE_SAFE, DEFAULT_CONFIG)
        self.assertEqual(direction, DIRECTION_UP)

    def test_building_momentum_does_not_count_as_positive(self):
        """Reproduces TNON's entry: momentum label is "Building" (fires
        on momentum_acceleration > 0 regardless of slope_5m), which
        looks bullish but can mean the price is still falling. Must not
        satisfy the momentum leg of the unanimity check."""
        direction, confidence = _direction_and_confidence(
            "Building", "Expanding", "Bullish", "Bullish", EXTENSION_NORMAL, RESISTANCE_SAFE, DEFAULT_CONFIG)
        self.assertNotEqual(direction, DIRECTION_UP)

    def test_resistance_caution_blocks_up_even_with_everything_else_positive(self):
        """CAUTION (closer than the safe distance, but not yet TOO_CLOSE)
        used to only cost a 0.95x confidence penalty. Now it must block
        entirely, same as TOO_CLOSE -- "enough space to profit, otherwise
        wait for a confirmed breakout." Breakout-in-progress still
        collapses to SAFE upstream in _resistance_classification(), so
        this only affects the non-breakout CAUTION case."""
        direction, confidence = _direction_and_confidence(
            "Strong", "Expanding", "Bullish", "Bullish", EXTENSION_NORMAL, RESISTANCE_CAUTION, DEFAULT_CONFIG)
        self.assertNotEqual(direction, DIRECTION_UP)

    def test_vwap_and_ema9_no_longer_gate_direction(self):
        """[ADJUSTED 2026-09-14] vwap and ema9 are deliberately excluded
        from the hard gate now -- a Bearish vwap and a "Reclaiming" (not
        yet confirmed Bullish) ema9 reading must NOT block UP as long as
        momentum, volume, and resistance are all positive. This is a
        real behavior change from the initial 5-condition version --
        confirms it took effect."""
        direction, confidence = _direction_and_confidence(
            "Strong", "Expanding", "Bearish", "Reclaiming", EXTENSION_NORMAL, RESISTANCE_SAFE, DEFAULT_CONFIG)
        self.assertEqual(direction, DIRECTION_UP)

    def test_vwap_and_ema9_still_feed_the_descriptive_confidence_blend(self):
        """Even though they no longer gate entry, vwap/ema9 must still
        move the blended confidence score (still used for shortlist
        ranking/logging) -- a Bearish vwap + Reclaiming ema9 reading
        should score meaningfully lower than an all-Bullish one, all
        else equal."""
        _, confidence_all_bullish = _direction_and_confidence(
            "Strong", "Expanding", "Bullish", "Bullish", EXTENSION_NORMAL, RESISTANCE_SAFE, DEFAULT_CONFIG)
        _, confidence_weak_vwap_ema = _direction_and_confidence(
            "Strong", "Expanding", "Bearish", "Reclaiming", EXTENSION_NORMAL, RESISTANCE_SAFE, DEFAULT_CONFIG)
        self.assertLess(confidence_weak_vwap_ema, confidence_all_bullish)


if __name__ == "__main__":
    unittest.main()
