"""
Smoke tests for analysis/price_action.py.

Verifies pattern extraction from tech dicts and direction-aware scoring.

Run:
    cd ~/apex_v16
    python -m tests.test_price_action
"""

from __future__ import annotations

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from analysis.price_action import (
    PASignals,
    PAScore,
    PATTERN_WEIGHTS,
    extract_pa_signals,
    score_pa,
)


def _ok(label: str) -> None:
    print(f"  ok  {label}")


# ============================================================
# 1. Empty tech -> all flags False, score neutral
# ============================================================

def test_empty_tech() -> None:
    sigs = extract_pa_signals({})
    assert isinstance(sigs, PASignals)
    assert not any([sigs.hammer, sigs.bull_engulfing, sigs.morning_star,
                    sigs.shooting_star, sigs.bear_engulfing, sigs.evening_star,
                    sigs.doji_bull, sigs.doji_bear])
    assert sigs.candle_strength == 1.0
    assert sigs.volume_weak is False

    score = score_pa(sigs, "BUY")
    assert score.bullish_score == 0
    assert score.bearish_score == 0
    assert score.dominant == "NEUTRAL"
    assert score.strength == 0
    assert score.favor_direction == []
    assert score.adverse_direction == []
    _ok("empty tech -> neutral score")


# ============================================================
# 2. Bullish patterns dominate -> BULLISH dominant
# ============================================================

def test_bullish_dominates() -> None:
    tech = {"hammer": True, "bull_engulfing": True, "candle_strength": 1.4}
    sigs = extract_pa_signals(tech)
    assert sigs.hammer and sigs.bull_engulfing
    assert sigs.candle_strength == 1.4

    score = score_pa(sigs, "BUY")
    expected_bull = PATTERN_WEIGHTS["hammer"] + PATTERN_WEIGHTS["bull_engulfing"]
    assert abs(score.bullish_score - expected_bull) < 1e-6
    assert score.bearish_score == 0
    assert score.dominant == "BULLISH"
    assert score.strength == round(expected_bull, 2)
    # For a BUY, bullish patterns are favoring
    assert "hammer" in score.favor_direction
    assert "bull_engulfing" in score.favor_direction
    assert score.adverse_direction == []
    _ok("bullish patterns dominate")


# ============================================================
# 3. Bearish patterns dominate -> BEARISH, adverse to BUY
# ============================================================

def test_bearish_adverse_to_buy() -> None:
    tech = {"shooting_star": True, "bear_engulfing": True, "evening_star": True}
    sigs = extract_pa_signals(tech)

    score = score_pa(sigs, "BUY")
    expected_bear = sum(PATTERN_WEIGHTS[k] for k in
                        ("shooting_star", "bear_engulfing", "evening_star"))
    assert abs(score.bearish_score - expected_bear) < 1e-6
    assert score.dominant == "BEARISH"
    # For a BUY, bearish patterns are ADVERSE
    assert score.favor_direction == []
    assert set(score.adverse_direction) == {"shooting_star",
                                             "bear_engulfing",
                                             "evening_star"}
    _ok("bearish patterns adverse to BUY")


# ============================================================
# 4. Direction inversion: same signals, SELL -> bearish FAVOR
# ============================================================

def test_direction_inverts_favor_adverse() -> None:
    tech = {"shooting_star": True, "bear_engulfing": True}
    sigs = extract_pa_signals(tech)

    buy_score = score_pa(sigs, "BUY")
    sell_score = score_pa(sigs, "SELL")

    # Scores are direction-independent
    assert buy_score.bullish_score == sell_score.bullish_score
    assert buy_score.bearish_score == sell_score.bearish_score
    assert buy_score.dominant == sell_score.dominant == "BEARISH"
    # But favor/adverse swap
    assert set(buy_score.adverse_direction) == set(sell_score.favor_direction)
    assert set(sell_score.adverse_direction) == set(buy_score.favor_direction)
    _ok("favor/adverse swap on direction flip")


# ============================================================
# 5. Doji split via doji_type
# ============================================================

def test_doji_split_by_type() -> None:
    tech_bull = {"doji": True, "doji_type": "bull"}
    tech_bear = {"doji": True, "doji_type": "bear"}
    tech_none = {"doji": True}                # no type -> neither
    tech_off  = {"doji": False, "doji_type": "bull"}  # not active

    assert extract_pa_signals(tech_bull).doji_bull is True
    assert extract_pa_signals(tech_bull).doji_bear is False
    assert extract_pa_signals(tech_bear).doji_bear is True
    assert extract_pa_signals(tech_bear).doji_bull is False
    assert extract_pa_signals(tech_none).doji_bull is False
    assert extract_pa_signals(tech_none).doji_bear is False
    assert extract_pa_signals(tech_off).doji_bull is False
    _ok("doji split by doji_type")


# ============================================================
# 6. Mixed/tied scores -> NEUTRAL with strength 0
# ============================================================

def test_tied_scores_neutral() -> None:
    # hammer (1.0) vs shooting_star (1.0) -> tie
    tech = {"hammer": True, "shooting_star": True}
    sigs = extract_pa_signals(tech)
    score = score_pa(sigs, "BUY")
    assert score.bullish_score == 1.0
    assert score.bearish_score == 1.0
    assert score.dominant == "NEUTRAL"
    assert score.strength == 0
    # But favor/adverse still classify correctly
    assert score.favor_direction == ["hammer"]
    assert score.adverse_direction == ["shooting_star"]
    _ok("tied scores -> NEUTRAL strength 0")


# ============================================================
# RENDER EXPLICIT
# ============================================================

def test_render_explicit_active_marked() -> None:
    sig = PASignals(bull_engulfing=True, hammer=True)
    out = sig.render_explicit()
    assert "bull_engulfing=⭐(1.5)" in out
    assert "hammer=⭐(1.0)" in out
    _ok("render_explicit: active marked with ⭐+weight")


def test_render_explicit_inactive_shown() -> None:
    sig = PASignals()  # all False
    out = sig.render_explicit()
    assert "bull_engulfing=·(1.5)" in out
    assert "shooting_star=·(1.0)" in out
    assert "doji_bull=·(0.6)" in out
    _ok("render_explicit: inactive shown with ·+weight")


def test_render_explicit_two_lines() -> None:
    out = PASignals().render_explicit()
    assert out.count("\n") == 1
    assert out.startswith("Bullish:")
    assert "\nBearish:" in out
    _ok("render_explicit: 2 lines Bullish/Bearish")


# ============================================================
# RUN
# ============================================================

def main() -> int:
    print("test_price_action.py")
    test_empty_tech()
    test_bullish_dominates()
    test_bearish_adverse_to_buy()
    test_direction_inverts_favor_adverse()
    test_doji_split_by_type()
    test_tied_scores_neutral()
    test_render_explicit_active_marked()
    test_render_explicit_inactive_shown()
    test_render_explicit_two_lines()
    print("ALL 9 TESTS PASSED")
    return 0


if __name__ == "__main__":
    sys.exit(main())
