import asyncio
import json
import sqlite3
from decimal import Decimal

import pytest

from binance_quant.config import Settings
from binance_quant.models import AccountSnapshot, Balance, Candle, OrderSide, SymbolMetadata, TradingMode
from binance_quant.runner import run_trade_loop
from binance_quant.storage import Storage


class FakeExchange:
    def __init__(self, min_notional: Decimal = Decimal("5")) -> None:
        self.account_calls = 0
        self.exchange_info_calls = 0
        self.min_notional = min_notional
        self.orders = []

    def exchange_info(self, symbol: str) -> SymbolMetadata:
        self.exchange_info_calls += 1
        return SymbolMetadata(
            symbol=symbol,
            base_asset="BTC",
            quote_asset="USDT",
            min_notional=self.min_notional,
            min_qty=Decimal("0.00001"),
            step_size=Decimal("0.00001"),
        )

    def account(self) -> AccountSnapshot:
        self.account_calls += 1
        return AccountSnapshot(
            {
                "USDT": Balance("USDT", Decimal("100")),
                "BTC": Balance("BTC", Decimal("1")),
            }
        )

    def create_market_order(self, symbol, side, quote_amount, client_order_id):
        self.orders.append((symbol, side, quote_amount, client_order_id))
        return {"symbol": symbol, "status": "FILLED", "clientOrderId": client_order_id}


class FailingOrderExchange(FakeExchange):
    def create_market_order(self, symbol, side, quote_amount, client_order_id):
        self.orders.append((symbol, side, quote_amount, client_order_id))
        raise RuntimeError(
            "request failed: https://testnet.binance.vision/api/v3/order?"
            "symbol=BTCUSDT&signature=abc123 with secret"
        )


def candle(close: str, index: int) -> Candle:
    value = Decimal(close)
    return Candle("BTCUSDT", index, index + 1, value, value, value, value, Decimal("1"))


async def candle_stream(candles):
    for item in candles:
        yield item


async def raising_candle_stream():
    raise AssertionError("stream should not be consumed")
    yield


def table_count(database_path, table: str) -> int:
    with sqlite3.connect(database_path) as connection:
        return int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])


def test_dry_run_trade_loop_records_order(tmp_path):
    settings = Settings(
        mode=TradingMode.DRY_RUN,
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FakeExchange()
    candles = [candle(value, index) for index, value in enumerate(["5", "4", "3", "8"], start=1)]

    summary = asyncio.run(
        run_trade_loop(
            settings,
            exchange_client=exchange,
            candle_stream=candle_stream(candles),
            max_candles=4,
        )
    )

    assert summary.processed_candles == 4
    assert summary.processed_candles == summary.signals
    assert summary.approved + summary.rejected == summary.signals
    assert summary.orders == summary.approved
    assert summary.orders == 1
    assert exchange.account_calls == 0
    assert exchange.orders == []
    assert table_count(settings.database_path, "signals") == 4
    assert table_count(settings.database_path, "risk_decisions") == 4
    assert table_count(settings.database_path, "order_results") == 1
    assert Storage(settings.database_path).list_orders()[0]["status"] == "DRY_RUN"


def test_testnet_live_trade_loop_calls_exchange(tmp_path):
    settings = Settings(
        mode=TradingMode.TESTNET_LIVE,
        api_key="key",
        api_secret="secret",
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FakeExchange()
    candles = [candle(value, index) for index, value in enumerate(["5", "4", "3", "8"], start=1)]

    summary = asyncio.run(
        run_trade_loop(
            settings,
            exchange_client=exchange,
            candle_stream=candle_stream(candles),
            max_candles=4,
        )
    )

    assert summary.processed_candles == summary.signals
    assert summary.approved + summary.rejected == summary.signals
    assert summary.orders == summary.approved
    assert summary.orders == 1
    assert table_count(settings.database_path, "signals") == 4
    assert table_count(settings.database_path, "risk_decisions") == 4
    assert table_count(settings.database_path, "order_results") == 1
    assert exchange.orders[0][1] is OrderSide.BUY


def test_live_trade_loop_calls_exchange(tmp_path):
    settings = Settings(
        mode=TradingMode.LIVE,
        api_key="key",
        api_secret="secret",
        rest_base_url="https://api.binance.com/api",
        stream_base_url="wss://stream.binance.com:9443/ws",
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FakeExchange()
    candles = [candle(value, index) for index, value in enumerate(["5", "4", "3", "8"], start=1)]

    summary = asyncio.run(
        run_trade_loop(
            settings,
            exchange_client=exchange,
            candle_stream=candle_stream(candles),
            max_candles=4,
        )
    )

    assert summary.processed_candles == summary.signals
    assert summary.approved + summary.rejected == summary.signals
    assert summary.orders == summary.approved
    assert summary.orders == 1
    assert exchange.account_calls == 4
    assert table_count(settings.database_path, "order_results") == 1
    assert exchange.orders[0][1] is OrderSide.BUY


def test_failed_testnet_live_order_is_persisted_with_sanitized_raw_json(tmp_path):
    settings = Settings(
        mode=TradingMode.TESTNET_LIVE,
        api_key="key",
        api_secret="secret",
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FailingOrderExchange()
    candles = [candle(value, index) for index, value in enumerate(["5", "4", "3", "8"], start=1)]

    with pytest.raises(RuntimeError, match="request failed"):
        asyncio.run(
            run_trade_loop(
                settings,
                exchange_client=exchange,
                candle_stream=candle_stream(candles),
                max_candles=4,
            )
        )

    assert table_count(settings.database_path, "signals") == 4
    assert table_count(settings.database_path, "risk_decisions") == 4
    assert table_count(settings.database_path, "order_results") == 1

    order = Storage(settings.database_path).list_orders()[0]
    assert order["status"] == "FAILED"
    assert order["dry_run"] == 0
    assert order["client_order_id"] == exchange.orders[0][3]

    raw_json = order["raw_json"]
    assert "signature=" not in raw_json
    assert "secret" not in raw_json
    assert "https://" not in raw_json
    raw = json.loads(raw_json)
    assert raw["error_type"] == "RuntimeError"
    assert raw["message"] == "order submission failed"


def test_zero_max_candles_does_not_consume_stream(tmp_path):
    settings = Settings(
        mode=TradingMode.DRY_RUN,
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FakeExchange()

    summary = asyncio.run(
        run_trade_loop(
            settings,
            exchange_client=exchange,
            candle_stream=raising_candle_stream(),
            max_candles=0,
        )
    )

    assert summary.processed_candles == 0
    assert summary.signals == 0
    assert summary.approved == 0
    assert summary.rejected == 0
    assert summary.orders == 0
    assert exchange.exchange_info_calls == 0
    assert exchange.orders == []
    assert table_count(settings.database_path, "signals") == 0
    assert table_count(settings.database_path, "risk_decisions") == 0
    assert table_count(settings.database_path, "order_results") == 0


def test_negative_max_candles_raises_before_stream_consumption(tmp_path):
    settings = Settings(
        mode=TradingMode.DRY_RUN,
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FakeExchange()

    with pytest.raises(ValueError, match="max_candles must be non-negative"):
        asyncio.run(
            run_trade_loop(
                settings,
                exchange_client=exchange,
                candle_stream=raising_candle_stream(),
                max_candles=-1,
            )
        )

    assert exchange.orders == []


def test_trade_loop_persists_rejections_without_orders(tmp_path):
    settings = Settings(
        mode=TradingMode.DRY_RUN,
        fast_window=2,
        slow_window=3,
        cooldown_seconds=0,
        quote_amount=Decimal("10"),
        max_quote_amount=Decimal("20"),
        database_path=tmp_path / "audit.sqlite3",
    )
    exchange = FakeExchange(min_notional=Decimal("100"))
    candles = [candle(value, index) for index, value in enumerate(["5", "4", "3", "8"], start=1)]

    summary = asyncio.run(
        run_trade_loop(
            settings,
            exchange_client=exchange,
            candle_stream=candle_stream(candles),
            max_candles=4,
        )
    )

    assert summary.processed_candles == 4
    assert summary.processed_candles == summary.signals
    assert summary.approved + summary.rejected == summary.signals
    assert summary.orders == summary.approved
    assert summary.approved == 0
    assert summary.rejected == 4
    assert summary.orders == 0
    assert exchange.orders == []
    assert table_count(settings.database_path, "signals") == 4
    assert table_count(settings.database_path, "risk_decisions") == 4
    assert table_count(settings.database_path, "order_results") == 0
