"""Integration tests for R:R scanner full flow with quality-based target selection.

Verifies the complete scan_ticker pipeline: quality-based S/R level selection,
correct TradeSetup field population, and database persistence.

**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 3.4**
"""

from __future__ import annotations

import json
from datetime import date, datetime, timedelta, timezone

import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.ohlcv import OHLCVRecord
from app.models.score import CompositeScore
from app.models.sr_level import SRLevel
from app.models.ticker import Ticker
from app.models.trade_setup import TradeSetup
from app.services.rr_scanner_service import scan_ticker


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
async def scan_session() -> AsyncSession:
    """Provide a DB session compatible with scan_ticker (which commits)."""
    from tests.conftest import _test_session_factory

    async with _test_session_factory() as session:
        yield session


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_ohlcv_bars(
    ticker_id: int,
    num_bars: int = 20,
    base_close: float = 100.0,
) -> list[OHLCVRecord]:
    """Generate OHLCV bars closing around base_close with ATR ≈ 2.0."""
    bars: list[OHLCVRecord] = []
    start = date(2024, 1, 1)
    for i in range(num_bars):
        close = base_close + (i % 3 - 1) * 0.5  # oscillate ±0.5
        bars.append(OHLCVRecord(
            ticker_id=ticker_id,
            date=start + timedelta(days=i),
            open=close - 0.3,
            high=close + 1.0,
            low=close - 1.0,
            close=close,
            volume=100_000,
        ))
    return bars


# ===========================================================================
# 8.1 Integration test: full scan_ticker flow with quality-based selection,
#     correct TradeSetup fields, and database persistence
# ===========================================================================

@pytest.mark.asyncio
async def test_scan_ticker_full_flow_quality_selection_and_persistence(
    scan_session: AsyncSession,
):
    """Integration test for the complete scan_ticker pipeline.

    Scenario:
    - Entry ≈ 100, ATR ≈ 2.0, risk ≈ 3.0 (atr_multiplier=1.5)
    - 3 resistance levels above (long candidates):
        A: price=105, strength=90 (strong, near)  → highest quality
        B: price=115, strength=40 (medium, mid)
        C: price=135, strength=5  (weak, far)
    - 3 support levels below (short candidates):
        D: price=95,  strength=85 (strong, near)  → highest quality
        E: price=85,  strength=35 (medium, mid)
        F: price=65,  strength=8  (weak, far)
    - CompositeScore: 72.5

    Verifies:
    1. Both long and short setups are produced
    2. Long target = Level A (highest quality, not most distant)
    3. Short target = Level D (highest quality, not most distant)
    4. All TradeSetup fields are correct and rounded to 4 decimals
    5. rr_ratio is the actual R:R of the selected level
    6. Old setups are deleted, new ones persisted
    """
    # -- Setup: create ticker --
    ticker = Ticker(symbol="INTEG")
    scan_session.add(ticker)
    await scan_session.flush()

    # -- Setup: OHLCV bars (20 bars, close ≈ 100, ATR ≈ 2.0) --
    bars = _make_ohlcv_bars(ticker.id, num_bars=20, base_close=100.0)
    scan_session.add_all(bars)

    # -- Setup: S/R levels --
    sr_levels = [
        # Long candidates (resistance above entry)
        SRLevel(ticker_id=ticker.id, price_level=105.0, type="resistance",
                strength=90, detection_method="volume_profile"),
        SRLevel(ticker_id=ticker.id, price_level=115.0, type="resistance",
                strength=40, detection_method="volume_profile"),
        SRLevel(ticker_id=ticker.id, price_level=135.0, type="resistance",
                strength=5, detection_method="pivot_point"),
        # Short candidates (support below entry)
        SRLevel(ticker_id=ticker.id, price_level=95.0, type="support",
                strength=85, detection_method="volume_profile"),
        SRLevel(ticker_id=ticker.id, price_level=85.0, type="support",
                strength=35, detection_method="pivot_point"),
        SRLevel(ticker_id=ticker.id, price_level=65.0, type="support",
                strength=8, detection_method="volume_profile"),
    ]
    scan_session.add_all(sr_levels)

    # -- Setup: CompositeScore --
    comp = CompositeScore(
        ticker_id=ticker.id,
        score=72.5,
        is_stale=False,
        weights_json="{}",
        computed_at=datetime.now(timezone.utc),
    )
    scan_session.add(comp)

    # -- Setup: dummy old setups that should be deleted --
    old_setup = TradeSetup(
        ticker_id=ticker.id,
        direction="long",
        entry_price=99.0,
        stop_loss=96.0,
        target=120.0,
        rr_ratio=7.0,
        composite_score=50.0,
        detected_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
    )
    scan_session.add(old_setup)
    await scan_session.commit()

    # Verify old setup exists before scan
    pre_result = await scan_session.execute(
        select(TradeSetup).where(TradeSetup.ticker_id == ticker.id)
    )
    pre_setups = list(pre_result.scalars().all())
    assert len(pre_setups) == 1, "Dummy old setup should exist before scan"

    # -- Act: run scan_ticker --
    setups = await scan_ticker(
        scan_session,
        "INTEG",
        rr_threshold=1.5,
        atr_multiplier=1.5,
        gate_levels_override=sr_levels,
    )

    # -- Assert: both directions produced --
    assert len(setups) == 2, f"Expected 2 setups (long + short), got {len(setups)}"

    long_setups = [s for s in setups if s.direction == "long"]
    short_setups = [s for s in setups if s.direction == "short"]
    assert len(long_setups) == 1, f"Expected 1 long setup, got {len(long_setups)}"
    assert len(short_setups) == 1, f"Expected 1 short setup, got {len(short_setups)}"

    long_setup = long_setups[0]
    short_setup = short_setups[0]

    # -- Assert: long target is Level A (highest quality, not most distant) --
    # Level A: price=105 (strong, near) should beat Level C: price=135 (weak, far)
    assert long_setup.target == pytest.approx(105.0, abs=0.01), (
        f"Long target should be 105.0 (highest quality), got {long_setup.target}"
    )

    # -- Assert: short target is Level D (highest quality, not most distant) --
    # Level D: price=95 (strong, near) should beat Level F: price=65 (weak, far)
    assert short_setup.target == pytest.approx(95.0, abs=0.01), (
        f"Short target should be 95.0 (highest quality), got {short_setup.target}"
    )

    # -- Assert: entry_price is the last close (≈ 100) --
    # Last bar: index 19, close = 100 + (19 % 3 - 1) * 0.5 = 100 + 0*0.5 = 100.0
    expected_entry = 100.0
    assert long_setup.entry_price == pytest.approx(expected_entry, abs=0.5)
    assert short_setup.entry_price == pytest.approx(expected_entry, abs=0.5)

    # -- Assert: stop_loss values --
    # ATR ≈ 2.0, risk = ATR × 1.5 = 3.0
    # Long stop = entry - risk, Short stop = entry + risk
    risk = long_setup.entry_price - long_setup.stop_loss
    assert risk > 0, "Long risk must be positive"
    assert short_setup.stop_loss > short_setup.entry_price, "Short stop must be above entry"

    # -- Assert: rr_ratio is the actual R:R of the selected level --
    long_reward = long_setup.target - long_setup.entry_price
    long_expected_rr = round(long_reward / risk, 4)
    assert long_setup.rr_ratio == pytest.approx(long_expected_rr, abs=0.01), (
        f"Long rr_ratio should be actual R:R={long_expected_rr}, got {long_setup.rr_ratio}"
    )

    short_risk = short_setup.stop_loss - short_setup.entry_price
    short_reward = short_setup.entry_price - short_setup.target
    short_expected_rr = round(short_reward / short_risk, 4)
    assert short_setup.rr_ratio == pytest.approx(short_expected_rr, abs=0.01), (
        f"Short rr_ratio should be actual R:R={short_expected_rr}, got {short_setup.rr_ratio}"
    )

    # -- Assert: composite_score matches --
    assert long_setup.composite_score == pytest.approx(72.5, abs=0.01)
    assert short_setup.composite_score == pytest.approx(72.5, abs=0.01)

    # -- Assert: ticker_id is correct --
    assert long_setup.ticker_id == ticker.id
    assert short_setup.ticker_id == ticker.id

    # -- Assert: detected_at is set --
    assert long_setup.detected_at is not None
    assert short_setup.detected_at is not None

    # -- Assert: fields are rounded to 4 decimal places --
    for setup in [long_setup, short_setup]:
        for field_name in ("entry_price", "stop_loss", "target", "rr_ratio", "composite_score"):
            val = getattr(setup, field_name)
            rounded = round(val, 4)
            assert val == pytest.approx(rounded, abs=1e-6), (
                f"{setup.direction} {field_name}={val} not rounded to 4 decimals"
            )

    # -- Assert: database persistence --
    # History is preserved: old setup remains, 2 new setups are appended
    db_result = await scan_session.execute(
        select(TradeSetup).where(TradeSetup.ticker_id == ticker.id)
    )
    persisted = list(db_result.scalars().all())
    assert len(persisted) == 3, (
        f"Expected 3 persisted setups (1 old + 2 new), got {len(persisted)}"
    )

    persisted_directions = sorted(s.direction for s in persisted)
    assert persisted_directions == ["long", "long", "short"], (
        f"Expected ['long', 'long', 'short'] persisted, got {persisted_directions}"
    )

    # Verify latest persisted records match returned setups
    persisted_long = max((s for s in persisted if s.direction == "long"), key=lambda s: s.id)
    persisted_short = max((s for s in persisted if s.direction == "short"), key=lambda s: s.id)

    assert persisted_long.target == long_setup.target
    assert persisted_long.rr_ratio == long_setup.rr_ratio
    assert persisted_long.entry_price == long_setup.entry_price
    assert persisted_long.stop_loss == long_setup.stop_loss
    assert persisted_long.composite_score == long_setup.composite_score

    assert persisted_short.target == short_setup.target
    assert persisted_short.rr_ratio == short_setup.rr_ratio
    assert persisted_short.entry_price == short_setup.entry_price
    assert persisted_short.stop_loss == short_setup.stop_loss
    assert persisted_short.composite_score == short_setup.composite_score


@pytest.mark.asyncio
async def test_scan_ticker_uses_transient_ladder_not_persisted_chart_levels(
    scan_session: AsyncSession,
    monkeypatch,
):
    ticker = Ticker(symbol="DUAL")
    scan_session.add(ticker)
    await scan_session.flush()
    scan_session.add_all(_make_ohlcv_bars(ticker.id, num_bars=20, base_close=100.0))
    scan_session.add(SRLevel(
        ticker_id=ticker.id,
        price_level=130.0,
        type="resistance",
        strength=100,
        detection_method="pivot_point",
    ))
    await scan_session.commit()

    ladder = [
        {
            "price_level": 105.0,
            "type": "resistance",
            "strength": 90,
            "detection_method": "range_grid",
            "sources": ["range_grid"],
            "rejection_count": 5,
            "last_rejection_age": None,
        },
        {
            "price_level": 95.0,
            "type": "support",
            "strength": 85,
            "detection_method": "range_grid",
            "sources": ["range_grid"],
            "rejection_count": 4,
            "last_rejection_age": None,
        },
    ]
    monkeypatch.setattr(
        "app.services.rr_scanner_service.detect_gate_target_ladder",
        lambda highs, lows, closes: ladder,
    )

    setups = await scan_ticker(
        scan_session,
        "DUAL",
        rr_threshold=1.5,
    )

    long_setup = next(setup for setup in setups if setup.direction == "long")
    assert long_setup.target == pytest.approx(105.0, abs=0.01)
    assert long_setup.target != pytest.approx(130.0, abs=0.01)
    targets = json.loads(long_setup.targets_json or "[]")
    assert targets
    assert all(target["sr_level_id"] < 0 for target in targets)
    assert all(target["sr_sources"] == ["range_grid"] for target in targets)
