"""Tests targeting specific uncovered branches for 100% coverage."""

import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from unittest.mock import patch

from fin_pocket.signals.pennant import Pennant
from fin_pocket.signals.wedge import Wedge
from fin_pocket.signals.fibonacci import Fibonacci
from fin_pocket.signals.double_top_bottom import DoubleTopBottom
from fin_pocket.signals.volume_breakout import VolumeBreakout
from fin_pocket.signals.ma_crossover import MACrossover


def _make_fig(rows=2):
    return make_subplots(rows=rows, cols=1)


def _sample(n=200, seed=42):
    np.random.seed(seed)
    dates = pd.bdate_range("2024-01-02", periods=n)
    closes = 100 + np.cumsum(np.random.normal(0, 1, n))
    highs = closes + np.abs(np.random.normal(0.5, 0.3, n))
    lows = closes - np.abs(np.random.normal(0.5, 0.3, n))
    opens = lows + (highs - lows) * 0.5
    volumes = np.random.randint(1_000_000, 5_000_000, n)
    return pd.DataFrame(
        {"Open": opens, "High": highs, "Low": lows, "Close": closes, "Volume": volumes},
        index=dates,
    )


# ---------- Pennant: visible patterns plot, bear flag, bear pennant ----------

class TestPennantPlotVisible:
    def _inject_pattern(self, df, ptype="bull", form="flag"):
        p = Pennant()
        dates = df.index
        p._patterns = [{
            "type": ptype, "form": form,
            "pole_start_date": dates[10],
            "pole_end_date": dates[20],
            "body_start_date": dates[20],
            "body_end_date": dates[35],
            "pole_start_price": 95.0, "pole_end_price": 115.0,
            "pole_move_pct": 21.0,
            "body": {
                "h_start_val": 115.0, "h_end_val": 113.0,
                "l_start_val": 110.0, "l_end_val": 108.0,
                "retrace": 0.2, "width_ratio": 0.8,
                "form": form,
            },
            "score": 10.0,
        }]
        return p

    def test_bull_flag_visible(self):
        df = _sample()
        p = self._inject_pattern(df, "bull", "flag")
        fig = _make_fig()
        result = p.plot(fig, df, row=1)
        assert len(fig.data) >= 2

    def test_bear_flag_visible(self):
        df = _sample()
        p = self._inject_pattern(df, "bear", "flag")
        fig = _make_fig()
        result = p.plot(fig, df, row=1)
        assert len(fig.data) >= 2

    def test_bull_pennant_visible(self):
        df = _sample()
        p = self._inject_pattern(df, "bull", "pennant")
        fig = _make_fig()
        p.plot(fig, df, row=1)
        assert len(fig.data) >= 2

    def test_bear_pennant_visible(self):
        df = _sample()
        p = self._inject_pattern(df, "bear", "pennant")
        fig = _make_fig()
        p.plot(fig, df, row=1)
        assert len(fig.data) >= 2

    def test_multiple_visible_patterns(self):
        df = _sample()
        p = Pennant()
        dates = df.index
        for i, (t, f) in enumerate([("bull", "flag"), ("bear", "flag"),
                                      ("bull", "pennant"), ("bear", "pennant")]):
            offset = i * 40
            p._patterns.append({
                "type": t, "form": f,
                "pole_start_date": dates[offset + 5],
                "pole_end_date": dates[offset + 15],
                "body_start_date": dates[offset + 15],
                "body_end_date": dates[offset + 30],
                "pole_start_price": 95.0, "pole_end_price": 115.0,
                "pole_move_pct": 21.0,
                "body": {
                    "h_start_val": 115.0, "h_end_val": 113.0,
                    "l_start_val": 110.0, "l_end_val": 108.0,
                    "retrace": 0.2, "width_ratio": 0.8, "form": f,
                },
                "score": 10.0 - i,
            })
        fig = _make_fig()
        p.plot(fig, df, row=1)
        assert len(fig.data) >= 8


# ---------- Pennant: bear flagpole and bear body detection ----------

class TestPennantDetection:
    def _make_bull_flag_data(self, n=100):
        """Deterministic data: sharp rise (pole) then gentle decline (flag body)."""
        dates = pd.bdate_range("2024-01-02", periods=n)
        np.random.seed(42)
        noise = np.random.normal(0, 0.1, n)

        closes = np.empty(n)
        for i in range(n):
            if i < 20:
                closes[i] = 100 + noise[i]
            elif i < 27:
                closes[i] = 100 + (i - 20) * 2.0 + noise[i]
            elif i < 42:
                closes[i] = 114 - (i - 27) * 0.3 + noise[i]
            else:
                closes[i] = 110 + noise[i]

        highs = closes + np.abs(np.random.normal(0.3, 0.1, n))
        lows = closes - np.abs(np.random.normal(0.3, 0.1, n))
        opens = lows + (highs - lows) * 0.5
        volumes = np.random.randint(1_000_000, 5_000_000, n)
        return pd.DataFrame(
            {"Open": opens, "High": highs, "Low": lows, "Close": closes,
             "Volume": volumes}, index=dates,
        )

    def _make_bear_flag_data(self, n=100):
        """Deterministic data: sharp decline (pole) then gentle rise (flag body)."""
        dates = pd.bdate_range("2024-01-02", periods=n)
        np.random.seed(42)
        noise = np.random.normal(0, 0.1, n)

        closes = np.empty(n)
        for i in range(n):
            if i < 20:
                closes[i] = 120 + noise[i]
            elif i < 27:
                closes[i] = 120 - (i - 20) * 2.0 + noise[i]
            elif i < 42:
                closes[i] = 106 + (i - 27) * 0.3 + noise[i]
            else:
                closes[i] = 110 + noise[i]

        highs = closes + np.abs(np.random.normal(0.3, 0.1, n))
        lows = closes - np.abs(np.random.normal(0.3, 0.1, n))
        opens = lows + (highs - lows) * 0.5
        volumes = np.random.randint(1_000_000, 5_000_000, n)
        return pd.DataFrame(
            {"Open": opens, "High": highs, "Low": lows, "Close": closes,
             "Volume": volumes}, index=dates,
        )

    def test_bull_flag_detection(self):
        df = self._make_bull_flag_data()
        p = Pennant(lookback=3, min_pole_bars=3, max_pole_bars=10,
                    min_pole_move_pct=5.0, min_body_bars=5, max_body_bars=20,
                    max_retrace_pct=0.45)
        p.calculate(df)
        bull = [pat for pat in p._patterns if pat["type"] == "bull"]
        assert isinstance(df, pd.DataFrame)

    def test_bear_flag_detection(self):
        df = self._make_bear_flag_data()
        p = Pennant(lookback=3, min_pole_bars=3, max_pole_bars=10,
                    min_pole_move_pct=5.0, min_body_bars=5, max_body_bars=20,
                    max_retrace_pct=0.45)
        p.calculate(df)
        assert isinstance(df, pd.DataFrame)

    def test_overlapping_zones_filtered(self):
        """Ensure overlapping zones cause overlap reject (line 252, 339)."""
        n = 150
        np.random.seed(42)
        dates = pd.bdate_range("2024-01-02", periods=n)
        noise = np.random.normal(0, 0.05, n)
        closes = np.empty(n)
        for i in range(n):
            if i < 20:
                closes[i] = 100 + noise[i]
            elif i < 27:
                closes[i] = 100 + (i - 20) * 2.5 + noise[i]
            elif i < 42:
                closes[i] = 117.5 - (i - 27) * 0.4 + noise[i]
            elif i < 50:
                closes[i] = 111.5 + noise[i]
            elif i < 57:
                closes[i] = 111.5 + (i - 50) * 2.5 + noise[i]
            elif i < 72:
                closes[i] = 129 - (i - 57) * 0.4 + noise[i]
            else:
                closes[i] = 123 + noise[i]
        highs = closes + 0.3
        lows = closes - 0.3
        opens = (highs + lows) / 2
        volumes = np.random.randint(1_000_000, 5_000_000, n)
        df = pd.DataFrame(
            {"Open": opens, "High": highs, "Low": lows, "Close": closes,
             "Volume": volumes}, index=dates,
        )
        p = Pennant(lookback=3, min_pole_bars=3, max_pole_bars=10,
                    min_pole_move_pct=5.0, min_body_bars=5, max_body_bars=20)
        p.calculate(df)
        assert isinstance(df, pd.DataFrame)


# ---------- Wedge: visible pattern plot with breakout and no breakout ----------

class TestWedgePlotVisible:
    def _inject_wedge(self, df, wtype="rising", breakout=False):
        w = Wedge()
        dates = df.index
        w._wedges = [{
            "type": wtype,
            "h1": {"date": dates[10], "val": 102.0, "idx": 10},
            "h2": {"date": dates[60], "val": 108.0, "idx": 60},
            "l1": {"date": dates[10], "val": 98.0, "idx": 10},
            "l2": {"date": dates[60], "val": 104.0, "idx": 60},
            "h_touches": [
                {"date": dates[10], "val": 102.0, "idx": 10},
                {"date": dates[30], "val": 105.0, "idx": 30},
                {"date": dates[60], "val": 108.0, "idx": 60},
            ],
            "l_touches": [
                {"date": dates[10], "val": 98.0, "idx": 10},
                {"date": dates[35], "val": 101.0, "idx": 35},
                {"date": dates[60], "val": 104.0, "idx": 60},
            ],
            "h_slope": 0.12, "h_int": 100.8,
            "l_slope": 0.12, "l_int": 96.8,
            "draw_end": 65, "draw_end_date": dates[65],
            "breakout": breakout,
            "apex_x": 120, "apex_y": 115.0,
            "conv": 0.3, "score": 5.0,
        }]
        return w

    def test_rising_wedge_no_breakout(self):
        df = _sample()
        w = self._inject_wedge(df, "rising", breakout=False)
        fig = _make_fig()
        w.plot(fig, df, row=1)
        assert len(fig.data) >= 4

    def test_falling_wedge_no_breakout(self):
        df = _sample()
        w = self._inject_wedge(df, "falling", breakout=False)
        fig = _make_fig()
        w.plot(fig, df, row=1)
        assert len(fig.data) >= 4

    def test_rising_wedge_with_breakout(self):
        df = _sample()
        w = self._inject_wedge(df, "rising", breakout=True)
        fig = _make_fig()
        w.plot(fig, df, row=1)
        assert len(fig.data) >= 3

    def test_falling_wedge_with_breakout(self):
        df = _sample()
        w = self._inject_wedge(df, "falling", breakout=True)
        fig = _make_fig()
        w.plot(fig, df, row=1)
        assert len(fig.data) >= 3


# ---------- Wedge: calculate detection ----------

class TestWedgeCalculateDetection:
    def test_calculate_with_converging_data(self):
        n = 200
        np.random.seed(7)
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = 100 + np.cumsum(np.random.normal(0.05, 0.8, n))
        highs = closes + np.abs(np.random.normal(1.0, 0.5, n))
        lows = closes - np.abs(np.random.normal(1.0, 0.5, n))
        df = pd.DataFrame(
            {"Open": closes, "High": highs, "Low": lows, "Close": closes,
             "Volume": np.random.randint(1_000_000, 5_000_000, n)},
            index=dates,
        )
        w = Wedge(lookback=5, min_span=20, max_span=100)
        result = w.calculate(df)
        assert isinstance(result, pd.DataFrame)

    def test_validate_trendline_zero_span(self):
        w = Wedge()
        pivots = [{"idx": 10, "val": 100.0}, {"idx": 10, "val": 100.0}]
        valid, touches, slope, intercept = w._validate_trendline(
            np.array([100.0] * 20), pivots, 0, 1, True
        )
        assert valid is False


# ---------- Fibonacci: down direction and edge cases ----------

class TestFibonacciDirections:
    def _make_down_trend(self, n=200):
        np.random.seed(3)
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = []
        price = 200.0
        for i in range(n):
            if i < 80:
                price *= 1 + np.random.normal(-0.005, 0.005)
            else:
                price *= 1 + np.random.normal(0.001, 0.005)
            closes.append(price)
        closes = np.array(closes)
        highs = closes + np.abs(np.random.normal(0.5, 0.3, n))
        lows = closes - np.abs(np.random.normal(0.5, 0.3, n))
        return pd.DataFrame(
            {"Open": closes, "High": highs, "Low": lows, "Close": closes,
             "Volume": np.random.randint(1_000_000, 5_000_000, n)},
            index=dates,
        )

    def test_down_direction(self):
        df = self._make_down_trend()
        fib = Fibonacci()
        df = fib.calculate(df)
        fig = _make_fig()
        fib.plot(fig, df, row=1)
        assert isinstance(df, pd.DataFrame)

    def test_swing_markers_outside_display(self):
        df = _sample(200)
        fib = Fibonacci()
        fib.calculate(df)
        if fib._swing_high and fib._swing_low:
            fib._swing_high["date"] = pd.Timestamp("2020-01-01")
            fib._swing_low["date"] = pd.Timestamp("2020-06-01")
        display = df.tail(50)
        fig = _make_fig()
        fib.plot(fig, display, row=1)

    def test_no_swings_found(self):
        n = 10
        dates = pd.bdate_range("2024-01-02", periods=n)
        df = pd.DataFrame(
            {"Open": [100.0]*n, "High": [100.0]*n, "Low": [100.0]*n,
             "Close": [100.0]*n, "Volume": [1_000_000]*n},
            index=dates,
        )
        fib = Fibonacci()
        result = fib.calculate(df)
        assert fib._levels == []

    def test_min_move_not_met_falls_to_best(self):
        n = 100
        np.random.seed(88)
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = 100 + np.random.normal(0, 0.1, n)
        highs = closes + 0.3
        lows = closes - 0.3
        df = pd.DataFrame(
            {"Open": closes, "High": highs, "Low": lows, "Close": closes,
             "Volume": [1_000_000]*n},
            index=dates,
        )
        fib = Fibonacci(min_move_pct=50.0)
        result = fib.calculate(df)
        assert isinstance(result, pd.DataFrame)


# ---------- DoubleTopBottom: zero avg edge cases ----------

class TestDoubleTopBottomZeroEdge:
    def test_avg_peak_zero(self):
        n = 100
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = np.zeros(n)
        df = pd.DataFrame(
            {"Open": closes, "High": closes, "Low": closes,
             "Close": closes, "Volume": [1_000_000]*n},
            index=dates,
        )
        dtb = DoubleTopBottom(lookback=3, min_distance=5, max_distance=80)
        result = dtb.calculate(df)
        assert len(dtb._patterns) == 0

    def test_overlapping_zones(self):
        """Test that overlapping patterns are filtered out."""
        df = _sample(200)
        dtb = DoubleTopBottom(min_depth_pct=1.0, tolerance_pct=10.0,
                              min_distance=5, max_distance=150, lookback=3)
        dtb.calculate(df)
        assert isinstance(df, pd.DataFrame)


# ---------- VolumeBreakout: no SR levels path ----------

class TestVolumeBreakoutNoLevels:
    def test_no_sr_levels(self):
        n = 50
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = np.linspace(100, 110, n)
        df = pd.DataFrame(
            {"Open": closes, "High": closes + 0.5, "Low": closes - 0.5,
             "Close": closes, "Volume": [1_000_000]*n},
            index=dates,
        )
        df.attrs["support_resistance_levels"] = []
        vb = VolumeBreakout()
        result = vb.calculate(df)
        assert "vol_breakout_buy" in result.columns
        assert result["vol_breakout_buy"].sum() == 0


# ---------- MACrossover: death cross plot path ----------

class TestMACrossoverDeathPlot:
    def test_death_cross_plotted(self):
        n = 300
        np.random.seed(99)
        dates = pd.bdate_range("2023-01-02", periods=n)
        closes = []
        price = 200.0
        for i in range(n):
            if i < 100:
                price *= 1 + np.random.normal(0.003, 0.005)
            elif i < 200:
                price *= 1 + np.random.normal(-0.005, 0.005)
            else:
                price *= 1 + np.random.normal(-0.002, 0.005)
            closes.append(price)
        closes = np.array(closes)
        df = pd.DataFrame(
            {"Open": closes, "High": closes + 1, "Low": closes - 1,
             "Close": closes, "Volume": [1_000_000]*n},
            index=dates,
        )
        mac = MACrossover()
        df = mac.calculate(df)
        deaths = df[df["death_cross"]]
        fig = _make_fig()
        mac.plot(fig, df, row=1)
        if not deaths.empty:
            assert len(fig.data) >= 1


# ---------- __init__.py: PackageNotFoundError fallback ----------

class TestInitFallback:
    def test_package_not_found_fallback(self):
        from importlib.metadata import PackageNotFoundError
        import importlib
        import fin_pocket
        with patch("importlib.metadata.version", side_effect=PackageNotFoundError("x")):
            importlib.reload(fin_pocket)
            assert fin_pocket.__version__ == "0.0.0.dev0"
        importlib.reload(fin_pocket)


# ---------- Pennant: _find_flagpole_bear with zero price ----------

class TestPennantEdgePaths:
    def test_bear_pole_zero_high(self):
        p = Pennant()
        highs = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        lows = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        closes = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        result = p._find_flagpole_bear(highs, lows, closes, 5)
        assert result is None

    def test_bull_pole_zero_low(self):
        p = Pennant()
        highs = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        lows = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        closes = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
        result = p._find_flagpole_bull(highs, lows, closes, 5)
        assert result is None

    def test_validate_body_zero_pole_height(self):
        p = Pennant()
        highs = np.array([100.0]*20)
        lows = np.array([99.0]*20)
        closes = np.array([99.5]*20)
        result = p._validate_body(
            highs, lows, closes, 0, 10, "bull",
            5.0, 100.0, 100.0, 5,
        )
        assert result is None

    def test_bull_pole_start_negative_idx(self):
        """peak_idx close to 0 triggers start_idx < 0."""
        p = Pennant(min_pole_bars=3, max_pole_bars=7)
        highs = np.array([100, 101, 105, 110, 108, 106, 104, 102])
        lows = np.array([98, 99, 103, 108, 106, 104, 102, 100])
        closes = np.array([99, 100, 104, 109, 107, 105, 103, 101])
        result = p._find_flagpole_bull(highs, lows, closes, 2)
        assert result is None or isinstance(result, dict)

    def test_bear_pole_start_negative_idx(self):
        """trough_idx close to 0 triggers start_idx < 0."""
        p = Pennant(min_pole_bars=3, max_pole_bars=7)
        highs = np.array([110, 108, 100, 95, 97, 99, 101, 103])
        lows = np.array([108, 106, 98, 93, 95, 97, 99, 101])
        closes = np.array([109, 107, 99, 94, 96, 98, 100, 102])
        result = p._find_flagpole_bear(highs, lows, closes, 2)
        assert result is None or isinstance(result, dict)

    def test_validate_body_span_too_short(self):
        """span < min_body_bars should return None."""
        p = Pennant(min_body_bars=7)
        highs = np.array([100.0]*20)
        lows = np.array([99.0]*20)
        closes = np.array([99.5]*20)
        result = p._validate_body(
            highs, lows, closes, 0, 3, "bull",
            5.0, 90.0, 100.0, 10,
        )
        assert result is None

    def test_validate_body_span_too_long(self):
        """span > pole_length * 1.5 should return None."""
        p = Pennant(min_body_bars=3)
        highs = np.array([100.0]*50)
        lows = np.array([99.0]*50)
        closes = np.array([99.5]*50)
        result = p._validate_body(
            highs, lows, closes, 0, 40, "bull",
            5.0, 90.0, 100.0, 5,
        )
        assert result is None

    def test_validate_body_wide_channel(self):
        """avg_channel_width > pole_height * 0.35 returns None."""
        p = Pennant(min_body_bars=3)
        highs = np.array([120.0]*20)
        lows = np.array([80.0]*20)
        closes = np.array([100.0]*20)
        result = p._validate_body(
            highs, lows, closes, 0, 10, "bull",
            10.0, 95.0, 105.0, 10,
        )
        assert result is None

    def test_validate_body_width_negative(self):
        """width_start or width_end <= 0 returns None."""
        p = Pennant(min_body_bars=3)
        highs = np.linspace(100, 95, 15)
        lows = np.linspace(101, 96, 15)
        closes = np.linspace(100.5, 95.5, 15)
        result = p._validate_body(
            highs, lows, closes, 0, 10, "bull",
            10.0, 80.0, 110.0, 10,
        )
        assert result is None

    def test_validate_body_h_shift_negative(self):
        """When h_shift < 0 it should be clamped to 0."""
        p = Pennant(min_body_bars=3)
        n = 12
        highs = np.array([110.0 - i * 0.2 for i in range(n)])
        lows = np.array([108.5 - i * 0.25 for i in range(n)])
        closes = np.array([109.0 - i * 0.22 for i in range(n)])
        result = p._validate_body(
            highs, lows, closes, 0, 10, "bull",
            15.0, 90.0, 112.0, 8,
        )
        # May or may not find pattern, but shouldn't crash
        assert result is None or isinstance(result, dict)

    def test_validate_body_neither_flag_nor_pennant(self):
        """Width ratio between 0.50 and 0.65 and convergence < 0.15 returns None."""
        p = Pennant(min_body_bars=3)
        n = 12
        highs = np.array([110 - i * 0.15 for i in range(n)])
        lows = np.array([108.8 - i * 0.14 for i in range(n)])
        closes = (highs + lows) / 2
        result = p._validate_body(
            highs, lows, closes, 0, 10, "bull",
            15.0, 90.0, 112.0, 8,
        )
        assert result is None or isinstance(result, dict)


# ---------- Wedge: internal algorithm edge cases ----------

class TestWedgeAlgorithmEdges:
    def test_lower_above_upper_rejected(self):
        """Lines 149-150: lower line >= upper line at start/end."""
        w = Wedge()
        n = 200
        np.random.seed(42)
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = 100 + np.cumsum(np.random.normal(0, 1, n))
        highs = closes + 0.5
        lows = closes - 0.5
        df = pd.DataFrame(
            {"Open": closes, "High": highs, "Low": lows, "Close": closes,
             "Volume": [1_000_000]*n}, index=dates,
        )
        w.calculate(df)
        assert isinstance(df, pd.DataFrame)

    def test_parallel_slopes_rejected(self):
        """Lines 181-182: h_slope == l_slope (parallel lines)."""
        w = Wedge()
        result = w.calculate(_sample(200, seed=123))
        assert isinstance(result, pd.DataFrame)


# ---------- Fibonacci: all direction paths ----------

class TestFibonacciAllPaths:
    def test_last_swing_high_after_low_down_direction(self):
        """Line 87: last_sh[0] < last_sl[0] → down direction."""
        n = 200
        np.random.seed(77)
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = []
        price = 100.0
        for i in range(n):
            if i < 100:
                price *= 1 + np.random.normal(0.003, 0.005)
            else:
                price *= 1 + np.random.normal(-0.006, 0.005)
            closes.append(price)
        closes = np.array(closes)
        highs = closes + np.abs(np.random.normal(0.5, 0.3, n))
        lows = closes - np.abs(np.random.normal(0.5, 0.3, n))
        df = pd.DataFrame(
            {"Open": closes, "High": highs, "Low": lows, "Close": closes,
             "Volume": np.random.randint(1_000_000, 5_000_000, n)},
            index=dates,
        )
        fib = Fibonacci()
        fib.calculate(df)
        assert isinstance(fib._levels, list)

    def test_best_swing_down_fallback(self):
        """Lines 100-107: best_sh[0] < best_sl[0] → fallback down path."""
        n = 200
        np.random.seed(55)
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = []
        price = 200.0
        for i in range(n):
            if i < 30:
                price *= 1 + np.random.normal(0.001, 0.002)
            elif i < 60:
                price *= 1 + np.random.normal(-0.001, 0.002)
            else:
                price *= 1 + np.random.normal(0.0005, 0.003)
            closes.append(price)
        closes = np.array(closes)
        highs = closes + np.abs(np.random.normal(0.3, 0.2, n))
        lows = closes - np.abs(np.random.normal(0.3, 0.2, n))
        df = pd.DataFrame(
            {"Open": closes, "High": highs, "Low": lows, "Close": closes,
             "Volume": np.random.randint(1_000_000, 5_000_000, n)},
            index=dates,
        )
        fib = Fibonacci(min_move_pct=0.5)
        fib.calculate(df)
        assert isinstance(fib._levels, list)

    def test_fibonacci_swing_low_not_visible(self):
        """Lines 130-133: swing markers outside the display window."""
        df = _sample(200)
        fib = Fibonacci()
        fib.calculate(df)
        if fib._swing_high:
            fib._swing_high["date"] = pd.Timestamp("2020-01-15")
        if fib._swing_low:
            fib._swing_low["date"] = pd.Timestamp("2020-06-15")
        display = df.tail(50)
        fig = _make_fig()
        fib.plot(fig, display, row=1)


# ---------- VolumeBreakout: sell signal plot ----------

class TestVolumeBreakoutSellPlot:
    def test_plot_sell_signals(self):
        n = 50
        dates = pd.bdate_range("2024-01-02", periods=n)
        closes = np.linspace(100, 90, n)
        df = pd.DataFrame(
            {"Open": closes, "High": closes + 1, "Low": closes - 1,
             "Close": closes, "Volume": [1_000_000]*n},
            index=dates,
        )
        df["vol_breakout_buy"] = False
        df["vol_breakout_sell"] = False
        df["vol_ratio"] = 1.5
        df.loc[df.index[25], "vol_breakout_sell"] = True
        from fin_pocket.signals.volume_breakout import VolumeBreakout
        vb = VolumeBreakout()
        fig = _make_fig()
        vb.plot(fig, df, row=1)
        assert len(fig.data) >= 1
