from decimal import Decimal

import httpx
from binance_quant import __version__
from binance_quant.ai_trader import AiTradeBatchExecutionResult, AiTradeExecutionResult
from binance_quant.futures_models import FuturesExecutionResult
from binance_quant.futures_ai_trader import FuturesAiBatchResult
from binance_quant.models import AccountSnapshot, Balance
from binance_quant.runner import TradeLoopSummary
from typer.testing import CliRunner

from binance_quant.cli import app, _configure_stdio_encoding


runner = CliRunner()


def test_cli_configures_stdio_for_utf8_output():
    class Stream:
        def __init__(self):
            self.calls = []

        def reconfigure(self, **kwargs):
            self.calls.append(kwargs)

    stdout = Stream()
    stderr = Stream()

    _configure_stdio_encoding(stdout, stderr)

    assert stdout.calls == [{"encoding": "utf-8", "errors": "replace"}]
    assert stderr.calls == [{"encoding": "utf-8", "errors": "replace"}]


def test_version_command():
    result = runner.invoke(app, ["version"])

    assert result.exit_code == 0
    assert __version__ in result.stdout


def test_backtest_command_prints_metrics(tmp_path):
    csv_path = tmp_path / "candles.csv"
    csv_path.write_text(
        "\n".join(
            [
                "open_time,close_time,open,high,low,close,volume",
                "1,2,10,11,9,10,1",
                "2,3,10,12,9,11,1",
                "3,4,11,13,10,12,1",
                "4,5,12,14,11,11,1",
            ]
        ),
        encoding="utf-8",
    )

    result = runner.invoke(app, ["backtest", str(csv_path), "--fast-window", "2", "--slow-window", "3"])

    assert result.exit_code == 0
    assert "Candles" in result.stdout
    assert "4" in result.stdout
    assert "Buy signals" in result.stdout
    assert "Sell signals" in result.stdout
    assert "Hold signals" in result.stdout


def test_futures_backtest_command_prints_pnl_metrics(tmp_path):
    csv_path = tmp_path / "candles.csv"
    csv_path.write_text(
        "\n".join(
            [
                "open_time,close_time,open,high,low,close,volume",
                "1,2,5,5,5,5,1",
                "2,3,4,4,4,4,1",
                "3,4,3,3,3,3,1",
                "4,5,8,8,8,8,1",
                "5,6,9,9,9,9,1",
                "6,7,10,10,10,10,1",
            ]
        ),
        encoding="utf-8",
    )

    result = runner.invoke(
        app,
        [
            "futures-backtest",
            str(csv_path),
            "--fast-window",
            "2",
            "--slow-window",
            "3",
            "--initial-equity",
            "100",
            "--margin-amount",
            "10",
            "--leverage",
            "2",
            "--fee-rate",
            "0",
            "--slippage-rate",
            "0",
        ],
    )

    assert result.exit_code == 0
    assert "Candles" in result.stdout
    assert "Trades" in result.stdout
    assert "Win rate" in result.stdout
    assert "Gross PnL" in result.stdout
    assert "Net PnL" in result.stdout
    assert "Final equity" in result.stdout
    assert "Max drawdown" in result.stdout
    assert "105" in result.stdout


def test_futures_backtest_rejects_invalid_window_order(tmp_path):
    csv_path = tmp_path / "candles.csv"
    csv_path.write_text(
        "open_time,close_time,open,high,low,close,volume\n1,2,10,11,9,10,1\n",
        encoding="utf-8",
    )

    result = runner.invoke(app, ["futures-backtest", str(csv_path), "--fast-window", "3", "--slow-window", "3"])

    assert result.exit_code != 0
    assert "fast-window must be less than slow-window" in result.output


def test_futures_backtest_rejects_invalid_decimal_option(tmp_path):
    csv_path = tmp_path / "candles.csv"
    csv_path.write_text(
        "open_time,close_time,open,high,low,close,volume\n1,2,10,11,9,10,1\n",
        encoding="utf-8",
    )

    result = runner.invoke(app, ["futures-backtest", str(csv_path), "--initial-equity", "bad"])

    assert result.exit_code != 0
    assert "initial-equity must be a decimal number" in result.output


def test_backtest_rejects_invalid_window_order(tmp_path):
    csv_path = tmp_path / "candles.csv"
    csv_path.write_text(
        "open_time,close_time,open,high,low,close,volume\n1,2,10,11,9,10,1\n",
        encoding="utf-8",
    )

    result = runner.invoke(app, ["backtest", str(csv_path), "--fast-window", "3", "--slow-window", "3"])

    assert result.exit_code != 0
    assert "fast-window must be less than slow-window" in result.output


def test_orders_command_initializes_empty_database(tmp_path):
    db_path = tmp_path / "audit.sqlite3"

    result = runner.invoke(app, ["orders", "--database-path", str(db_path)])

    assert result.exit_code == 0
    assert "No orders found" in result.stdout


def test_account_command_prints_nonzero_balances(monkeypatch):
    class FakeClient:
        def __init__(self, **kwargs):
            pass

        def account(self):
            return AccountSnapshot(
                {
                    "BTC": Balance("BTC", Decimal("0.5"), Decimal("0")),
                    "ETH": Balance("ETH", Decimal("0"), Decimal("0")),
                    "USDT": Balance("USDT", Decimal("0"), Decimal("12.5")),
                }
            )

    monkeypatch.setattr("binance_quant.cli.BinanceSpotClient", FakeClient)

    result = runner.invoke(app, ["account"])

    assert result.exit_code == 0
    assert "BTC" in result.stdout
    assert "0.5" in result.stdout
    assert "USDT" in result.stdout
    assert "12.5" in result.stdout
    assert "ETH" not in result.stdout


def test_account_command_sanitizes_http_errors(monkeypatch):
    class FakeClient:
        def __init__(self, **kwargs):
            pass

        def account(self):
            request = httpx.Request("GET", "https://testnet.binance.vision/api/v3/account?timestamp=1&signature=secret")
            response = httpx.Response(401, request=request)
            raise httpx.HTTPStatusError("signed request failed", request=request, response=response)

    monkeypatch.setattr("binance_quant.cli.BinanceSpotClient", FakeClient)

    result = runner.invoke(app, ["account"])

    assert result.exit_code != 0
    assert "Binance API request failed" in result.output
    assert "signature=" not in result.output
    assert "/v3/account?" not in result.output
    assert "timestamp=1" not in result.output


def test_account_rejects_missing_config_path(tmp_path):
    missing_path = tmp_path / "missing.yml"

    result = runner.invoke(app, ["account", "--config", str(missing_path)])

    assert result.exit_code != 0
    assert "does not exist" in result.output


def test_sync_spot_trades_command_prints_summary(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: live\napi_key: key\napi_secret: secret\ndatabase_path: {db_path}\n",
        encoding="utf-8",
    )
    captured = {}

    class Result:
        status = "completed"
        reason = "spot trade sync completed"
        scanned_symbols = 2
        fetched_trades = 3
        inserted_trades = 2

    def fake_sync(settings, symbols=None):
        captured["database_path"] = settings.database_path
        captured["symbols"] = symbols
        return Result()

    monkeypatch.setattr("binance_quant.cli.sync_spot_trade_fills", fake_sync)

    result = runner.invoke(app, ["sync-spot-trades", "--config", str(config_path), "--symbol", "btcusdt"])

    assert result.exit_code == 0
    assert captured == {"database_path": db_path, "symbols": ("BTCUSDT",)}
    assert "Status: completed" in result.stdout
    assert "Reason: spot trade sync completed" in result.stdout
    assert "Scanned symbols: 2" in result.stdout
    assert "Fetched trades: 3" in result.stdout
    assert "Inserted trades: 2" in result.stdout


def test_sync_spot_trades_command_exits_nonzero_when_rejected(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: dry_run\n", encoding="utf-8")

    class Result:
        status = "rejected"
        reason = "live or testnet_live mode required"
        scanned_symbols = 0
        fetched_trades = 0
        inserted_trades = 0

    monkeypatch.setattr("binance_quant.cli.sync_spot_trade_fills", lambda settings, symbols=None: Result())

    result = runner.invoke(app, ["sync-spot-trades", "--config", str(config_path)])

    assert result.exit_code == 1
    assert "Status: rejected" in result.stdout
    assert "live or testnet_live mode required" in result.stdout


def test_trade_command_runs_loop_with_config(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: dry_run\nfast_window: 2\nslow_window: 3\ndatabase_path: {db_path}\n",
        encoding="utf-8",
    )
    captured = {}

    async def fake_run_trade_loop(settings, max_candles=None):
        captured["symbol"] = settings.symbol
        captured["max_candles"] = max_candles
        return TradeLoopSummary(processed_candles=1, signals=1, approved=0, rejected=1, orders=0)

    monkeypatch.setattr("binance_quant.cli.run_trade_loop", fake_run_trade_loop)

    result = runner.invoke(app, ["trade", "--config", str(config_path), "--max-candles", "1"])

    assert result.exit_code == 0
    assert captured == {"symbol": "BTCUSDT", "max_candles": 1}
    assert "Processed candles: 1" in result.stdout


def test_trade_command_sanitizes_http_errors(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: dry_run\nfast_window: 2\nslow_window: 3\ndatabase_path: {db_path}\n",
        encoding="utf-8",
    )

    async def fake_run_trade_loop(settings, max_candles=None):
        request = httpx.Request("GET", "https://testnet.binance.vision/api/v3/account?timestamp=1&signature=secret")
        response = httpx.Response(500, request=request)
        raise httpx.HTTPStatusError("signed request failed", request=request, response=response)

    monkeypatch.setattr("binance_quant.cli.run_trade_loop", fake_run_trade_loop)

    result = runner.invoke(app, ["trade", "--config", str(config_path), "--max-candles", "1"])

    assert result.exit_code != 0
    assert "Binance API request failed" in result.output
    assert "signature=" not in result.output
    assert "/v3/account?" not in result.output
    assert "timestamp=1" not in result.output


def test_trade_rejects_negative_max_candles():
    result = runner.invoke(app, ["trade", "--max-candles", "-1"])

    assert result.exit_code != 0
    assert "max-candles must be greater than or" in result.output
    assert "equal to 0" in result.output


def test_trade_rejects_missing_config_path(tmp_path):
    missing_path = tmp_path / "missing.yml"

    result = runner.invoke(app, ["trade", "--config", str(missing_path)])

    assert result.exit_code != 0
    assert "does not exist" in result.output


def test_live_auto_buy_once_command_prints_result(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: live\napi_key: key\napi_secret: secret\ndatabase_path: {db_path}\nlive_auto_buyer_enabled: false\n",
        encoding="utf-8",
    )

    class Result:
        run_id = 7
        status = "rejected"
        symbol = "BTCUSDT"
        reason = "live auto buyer disabled"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 7" in result.stdout
    assert "Status: rejected" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: live auto buyer disabled" in result.stdout


def test_live_auto_buy_once_command_exits_nonzero_for_pre_buy_data_error(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 8
        status = "rejected"
        symbol = ""
        reason = "pre-buy data error"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 8" in result.stdout
    assert "Status: rejected" in result.stdout
    assert "Symbol: -" in result.stdout
    assert "Reason: pre-buy data error" in result.stdout
    assert "signature=" not in result.output
    assert "/v3/order?" not in result.output
    assert "timestamp=1" not in result.output


def test_live_auto_buy_once_command_exits_nonzero_for_rollback_failed(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 9
        status = "rollback_failed"
        symbol = "BTCUSDT"
        reason = "protective stop failed; rollback failed"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 9" in result.stdout
    assert "Status: rollback_failed" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: protective stop failed; rollback failed" in result.stdout


def test_live_auto_buy_once_command_alerts_when_protection_fails(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_smtp_port: 25",
                "email_login: bot@example.com",
                "email_sender_password: secret-token",
                "email_recipient: ops@example.com",
                "",
            ]
        ),
        encoding="utf-8",
    )

    class Result:
        run_id = 9
        status = "rollback_failed"
        symbol = "BTCUSDT"
        reason = "protective stop failed; rollback failed"

    sent = []
    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())
    monkeypatch.setattr(
        "binance_quant.cli.send_auto_buy_protection_failure_email",
        lambda settings, result: sent.append((settings, result)),
    )

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert len(sent) == 1
    assert sent[0][0].email_notifications_enabled is True
    assert sent[0][1].status == "rollback_failed"
    assert "secret-token" not in result.output


def test_live_auto_buy_once_command_sanitizes_protection_alert_failures(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_smtp_port: 25",
                "email_login: bot@example.com",
                "email_sender_password: secret-token",
                "email_recipient: ops@example.com",
                "",
            ]
        ),
        encoding="utf-8",
    )

    class Result:
        run_id = 9
        status = "rollback_failed"
        symbol = "BTCUSDT"
        reason = "protective stop failed; rollback failed"

    def fail_email(settings, result):
        raise RuntimeError(f"auth failed for {settings.email_sender_password.get_secret_value()}")

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())
    monkeypatch.setattr("binance_quant.cli.send_auto_buy_protection_failure_email", fail_email)

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Email notification failed." in result.output
    assert "secret-token" not in result.output


def test_live_auto_buy_once_command_exits_nonzero_for_rollback_completed(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 10
        status = "rollback_completed"
        symbol = "BTCUSDT"
        reason = "protective stop failed; rollback completed"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 10" in result.stdout
    assert "Status: rollback_completed" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: protective stop failed; rollback completed" in result.stdout


def test_live_auto_buy_once_command_keeps_no_candidate_exit_zero(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 11
        status = "no_candidate"
        symbol = ""
        reason = "no candidate passed scanner"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 11" in result.stdout
    assert "Status: no_candidate" in result.stdout
    assert "Symbol: -" in result.stdout
    assert "Reason: no candidate passed scanner" in result.stdout


def test_live_auto_buy_once_command_keeps_risk_rejection_exit_zero(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 12
        status = "rejected"
        symbol = "BTCUSDT"
        reason = "spread above max"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 12" in result.stdout
    assert "Status: rejected" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: spread above max" in result.stdout


def test_live_auto_buy_once_command_sanitizes_http_errors(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    def fail(settings):
        request = httpx.Request("POST", "https://api.binance.com/api/v3/order?timestamp=1&signature=secret")
        response = httpx.Response(400, request=request)
        raise httpx.HTTPStatusError("signed request failed", request=request, response=response)

    monkeypatch.setattr("binance_quant.cli.run_live_auto_buy_once", fail)

    result = runner.invoke(app, ["live-auto-buy-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Binance API request failed" in result.output
    assert "signature=" not in result.output
    assert "/v3/order?" not in result.output
    assert "timestamp=1" not in result.output


def test_live_auto_sell_once_command_prints_result(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: live\napi_key: key\napi_secret: secret\ndatabase_path: {db_path}\nlive_auto_seller_enabled: false\n",
        encoding="utf-8",
    )

    class Result:
        run_id = 17
        status = "rejected"
        symbol = "BTCUSDT"
        reason = "live auto seller disabled"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_sell_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-sell-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 17" in result.stdout
    assert "Status: rejected" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: live auto seller disabled" in result.stdout


def test_live_auto_sell_once_command_exits_nonzero_for_pre_sell_data_error(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 18
        status = "rejected"
        symbol = ""
        reason = "pre-sell data error"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_sell_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-sell-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 18" in result.stdout
    assert "Status: rejected" in result.stdout
    assert "Symbol: -" in result.stdout
    assert "Reason: pre-sell data error" in result.stdout
    assert "signature=" not in result.output
    assert "/v3/order?" not in result.output
    assert "timestamp=1" not in result.output


def test_live_auto_sell_once_command_exits_nonzero_for_sell_failed(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 19
        status = "sell_failed"
        symbol = "BTCUSDT"
        reason = "sell submission failed"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_sell_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-sell-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 19" in result.stdout
    assert "Status: sell_failed" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: sell submission failed" in result.stdout


def test_live_auto_sell_once_command_keeps_no_candidate_exit_zero(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 20
        status = "no_candidate"
        symbol = ""
        reason = "no candidate passed scanner"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_sell_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-sell-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 20" in result.stdout
    assert "Status: no_candidate" in result.stdout
    assert "Symbol: -" in result.stdout
    assert "Reason: no candidate passed scanner" in result.stdout


def test_live_auto_sell_once_command_keeps_risk_rejection_exit_zero(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    class Result:
        run_id = 21
        status = "rejected"
        symbol = "BTCUSDT"
        reason = "open sell order exists"

    monkeypatch.setattr("binance_quant.cli.run_live_auto_sell_once", lambda settings: Result())

    result = runner.invoke(app, ["live-auto-sell-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 21" in result.stdout
    assert "Status: rejected" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Reason: open sell order exists" in result.stdout


def test_live_auto_sell_once_command_sanitizes_http_errors(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\n", encoding="utf-8")

    def fail(settings):
        request = httpx.Request("POST", "https://api.binance.com/api/v3/order?timestamp=1&signature=secret")
        response = httpx.Response(400, request=request)
        raise httpx.HTTPStatusError("signed request failed", request=request, response=response)

    monkeypatch.setattr("binance_quant.cli.run_live_auto_sell_once", fail)

    result = runner.invoke(app, ["live-auto-sell-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Binance API request failed" in result.output
    assert "signature=" not in result.output
    assert "/v3/order?" not in result.output
    assert "timestamp=1" not in result.output


def test_live_ai_trade_once_command_prints_result(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: live\napi_key: key\napi_secret: secret\ndatabase_path: {db_path}\nlive_ai_trader_enabled: true\n",
        encoding="utf-8",
    )

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_once",
        lambda settings: AiTradeExecutionResult(
            31,
            "protected",
            "BTCUSDT",
            "BUY",
            "AI: upside; protective oco accepted",
            Decimal("0.88"),
        ),
    )

    result = runner.invoke(app, ["live-ai-trade-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 31" in result.stdout
    assert "Status: protected" in result.stdout
    assert "Symbol: BTCUSDT" in result.stdout
    assert "Action: BUY" in result.stdout
    assert "Confidence: 0.88" in result.stdout
    assert "Reason: AI: upside; protective oco accepted" in result.stdout


def test_live_ai_trade_once_command_exits_nonzero_for_execution_failure(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\nlive_ai_trader_enabled: true\n", encoding="utf-8")

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_once",
        lambda settings: AiTradeExecutionResult(32, "sell_failed", "BTCUSDT", "SELL", "AI: downside; sell submission failed"),
    )

    result = runner.invoke(app, ["live-ai-trade-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 32" in result.stdout
    assert "Status: sell_failed" in result.stdout
    assert "Action: SELL" in result.stdout


def test_live_ai_trade_once_command_keeps_no_action_exit_zero(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\nlive_ai_trader_enabled: true\n", encoding="utf-8")

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_once",
        lambda settings: AiTradeExecutionResult(33, "no_action", "", "HOLD", "mixed market"),
    )

    result = runner.invoke(app, ["live-ai-trade-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Run ID: 33" in result.stdout
    assert "Status: no_action" in result.stdout
    assert "Symbol: -" in result.stdout
    assert "Action: HOLD" in result.stdout
    assert "Confidence: -" in result.stdout
    assert "Reason: mixed market" in result.stdout


def test_live_ai_trade_once_command_sends_email_notification(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_ai_trader_enabled: true",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_login: 19176654177@163.com",
                "email_sender_password: secret-token",
                "email_recipient: xia_qingbo@163.com",
            ]
        ),
        encoding="utf-8",
    )
    sent = []

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_once",
        lambda settings: AiTradeExecutionResult(34, "no_action", "", "HOLD", "mixed market"),
    )
    monkeypatch.setattr("binance_quant.cli.send_ai_trade_email", lambda settings, result: sent.append((settings, result)))

    result = runner.invoke(app, ["live-ai-trade-once", "--config", str(config_path)])

    assert result.exit_code == 0
    [(settings, sent_result)] = sent
    assert settings.email_notifications_enabled is True
    assert sent_result.run_id == 34


def test_live_ai_trade_once_command_exits_nonzero_when_email_fails_without_leaking_secret(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_ai_trader_enabled: true",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_login: 19176654177@163.com",
                "email_sender_password: secret-token",
                "email_recipient: xia_qingbo@163.com",
            ]
        ),
        encoding="utf-8",
    )

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_once",
        lambda settings: AiTradeExecutionResult(35, "no_action", "", "HOLD", "mixed market"),
    )

    def fail_email(settings, result):
        raise RuntimeError("email notification failed")

    monkeypatch.setattr("binance_quant.cli.send_ai_trade_email", fail_email)

    result = runner.invoke(app, ["live-ai-trade-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Run ID: 35" in result.stdout
    assert "Email notification failed" in result.output
    assert "secret-token" not in result.output


def test_live_ai_trade_batch_once_command_prints_summary_and_rows(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    db_path = tmp_path / "audit.sqlite3"
    config_path.write_text(
        f"mode: live\napi_key: key\napi_secret: secret\ndatabase_path: {db_path}\nlive_ai_trader_enabled: true\n",
        encoding="utf-8",
    )

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_batch_once",
        lambda settings: AiTradeBatchExecutionResult(
            status="completed",
            reason="AI批量交易完成",
            max_actions=5,
            prediction_count=2,
            executed_count=2,
            results=(
                AiTradeExecutionResult(1, "protected", "BTCUSDT", "BUY", "done", Decimal("0.93")),
                AiTradeExecutionResult(2, "sold", "ETHUSDT", "SELL", "done", Decimal("0.91")),
            ),
        ),
    )

    result = runner.invoke(app, ["live-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Status: completed" in result.stdout
    assert "Reason: AI批量交易完成" in result.stdout
    assert "Max actions: 5" in result.stdout
    assert "Executed actions: 2" in result.stdout
    assert "Predictions: 2" in result.stdout
    assert "BTCUSDT" in result.stdout
    assert "ETHUSDT" in result.stdout
    assert "protected" in result.stdout
    assert "sold" in result.stdout


def test_live_ai_trade_batch_once_action_results_table_prints_executed_column(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\nlive_ai_trader_enabled: true\n", encoding="utf-8")

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_batch_once",
        lambda settings: AiTradeBatchExecutionResult(
            status="completed",
            reason="AI批量交易完成",
            max_actions=5,
            prediction_count=2,
            executed_count=1,
            results=(
                AiTradeExecutionResult(1, "protected", "BTCUSDT", "BUY", "已下单", Decimal("0.93")),
                AiTradeExecutionResult(2, "rejected", "ETHUSDT", "SELL", "风控拒绝", Decimal("0.91")),
            ),
        ),
    )

    result = runner.invoke(app, ["live-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Action results" in result.stdout
    action_table = result.stdout[result.stdout.index("Action results") :]
    assert "Executed" in action_table
    assert "yes" in action_table
    assert "no" in action_table


def test_live_ai_trade_batch_once_command_prints_hold_results_at_bottom(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\nlive_ai_trader_enabled: true\n", encoding="utf-8")

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_batch_once",
        lambda settings: AiTradeBatchExecutionResult(
            status="completed",
            reason="AI批量交易完成",
            max_actions=5,
            prediction_count=3,
            executed_count=1,
            results=(
                AiTradeExecutionResult(1, "no_action", "BTCUSDT", "HOLD", "BTC震荡", Decimal("0.33")),
                AiTradeExecutionResult(2, "sold", "ETHUSDT", "SELL", "ETH转弱", Decimal("0.91")),
                AiTradeExecutionResult(3, "no_action", "BNBUSDT", "HOLD", "BNB观望", Decimal("0.42")),
            ),
        ),
    )

    result = runner.invoke(app, ["live-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    action_index = result.stdout.index("Action results")
    eth_index = result.stdout.index("ETHUSDT")
    hold_index = result.stdout.index("HOLD predictions")
    btc_index = result.stdout.index("BTCUSDT")
    bnb_index = result.stdout.index("BNBUSDT")
    assert action_index < eth_index < hold_index < btc_index < bnb_index
    assert "Executed" not in result.stdout[hold_index:]


def test_live_ai_trade_batch_once_command_exits_nonzero_for_execution_failure(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text("mode: live\napi_key: key\napi_secret: secret\nlive_ai_trader_enabled: true\n", encoding="utf-8")

    monkeypatch.setattr(
        "binance_quant.cli.run_live_ai_trade_batch_once",
        lambda settings: AiTradeBatchExecutionResult(
            status="failed",
            reason="AI批量交易存在失败",
            max_actions=5,
            prediction_count=1,
            executed_count=1,
            results=(
                AiTradeExecutionResult(9, "sell_failed", "BTCUSDT", "SELL", "failed", Decimal("0.91")),
            ),
        ),
    )

    result = runner.invoke(app, ["live-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 1
    assert "Status: failed" in result.stdout
    assert "sell_failed" in result.stdout


def test_live_ai_trade_batch_once_command_sends_email_notification(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_ai_trader_enabled: true",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_login: 19176654177@163.com",
                "email_sender_password: secret-token",
                "email_recipient: xia_qingbo@163.com",
            ]
        ),
        encoding="utf-8",
    )
    batch_result = AiTradeBatchExecutionResult(
        status="completed",
        reason="AI批量交易完成",
        max_actions=5,
        prediction_count=1,
        executed_count=1,
        results=(AiTradeExecutionResult(9, "sold", "BTCUSDT", "SELL", "done", Decimal("0.91")),),
    )
    sent = []

    monkeypatch.setattr("binance_quant.cli.run_live_ai_trade_batch_once", lambda settings: batch_result)
    monkeypatch.setattr("binance_quant.cli.send_ai_trade_batch_email", lambda settings, result: sent.append((settings, result)))

    result = runner.invoke(app, ["live-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    [(settings, sent_result)] = sent
    assert settings.email_notifications_enabled is True
    assert sent_result is batch_result


def test_live_ai_trade_batch_once_command_exits_nonzero_when_email_fails_without_leaking_secret(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_ai_trader_enabled: true",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_login: 19176654177@163.com",
                "email_sender_password: secret-token",
                "email_recipient: xia_qingbo@163.com",
            ]
        ),
        encoding="utf-8",
    )
    batch_result = AiTradeBatchExecutionResult(
        status="completed",
        reason="AI批量交易完成",
        max_actions=5,
        prediction_count=1,
        executed_count=1,
        results=(AiTradeExecutionResult(9, "sold", "BTCUSDT", "SELL", "done", Decimal("0.91")),),
    )

    monkeypatch.setattr("binance_quant.cli.run_live_ai_trade_batch_once", lambda settings: batch_result)

    def fail_email(settings, result):
        raise RuntimeError("email notification failed")

    monkeypatch.setattr("binance_quant.cli.send_ai_trade_batch_email", fail_email)

    result = runner.invoke(app, ["live-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Status: completed" in result.stdout
    assert "Email notification failed" in result.output
    assert "secret-token" not in result.output


def test_live_futures_ai_trade_batch_once_command_prints_summary_and_rows(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_futures_ai_trader_enabled: true",
                "live_futures_confirm_production: true",
            ]
        ),
        encoding="utf-8",
    )
    monkeypatch.setattr(
        "binance_quant.cli.run_live_futures_ai_trade_batch_once",
        lambda settings: FuturesAiBatchResult(
            status="completed",
            reason="AI合约批量交易完成",
            max_actions=5,
            prediction_count=2,
            executed_count=1,
            daily_realized_pnl=Decimal("0"),
            total_isolated_margin=Decimal("5"),
            results=(
                FuturesExecutionResult(
                    1,
                    "protected",
                    "BTCUSDT",
                    "OPEN_LONG",
                    "AI: BTC趋势增强; 执行: protected",
                    Decimal("0.88"),
                    3,
                    Decimal("5"),
                ),
                FuturesExecutionResult(2, "no_action", "ETHUSDT", "HOLD", "ETH震荡", Decimal("0.30")),
            ),
        ),
    )

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Status: completed" in result.stdout
    assert "Daily net PnL: 0" in result.stdout
    assert "Total isolated margin: 5" in result.stdout
    assert "RunStatus" not in result.stdout
    assert "ConfLevMarginClose%Reason" not in result.stdout
    assert "Action results" in result.stdout
    assert "HOLD predictions" in result.stdout
    assert result.stdout.index("Action results") < result.stdout.index("HOLD predictions")
    assert "| Summary" in result.stdout
    assert "| Reason" in result.stdout
    assert "| Run | Status" not in result.stdout
    assert "| Conf |" not in result.stdout
    assert "Run ID: 1; Status: protected; Symbol: BTCUSDT; Action: OPEN_LONG; Confidence: 0.88; Leverage: 3; Margin: 5; Close%: -" in result.stdout
    assert "Run ID: 2; Status: no_action; Symbol: ETHUSDT; Action: HOLD; Confidence: 0.3; Leverage: -; Margin: -; Close%: -" in result.stdout
    assert "Status" in result.stdout
    assert "BTCUSDT" in result.stdout
    assert "OPEN_LONG" in result.stdout
    assert "执行: protected" in result.stdout


def test_live_futures_ai_trade_batch_once_command_formats_decimal_without_scientific_notation(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_futures_ai_trader_enabled: true",
                "live_futures_confirm_production: true",
            ]
        ),
        encoding="utf-8",
    )
    monkeypatch.setattr(
        "binance_quant.cli.run_live_futures_ai_trade_batch_once",
        lambda settings: FuturesAiBatchResult(
            status="no_action",
            reason="无合约动作",
            max_actions=5,
            prediction_count=1,
            executed_count=0,
            daily_realized_pnl=Decimal("0E-8"),
            total_isolated_margin=Decimal("0E-8"),
            results=(
                FuturesExecutionResult(
                    1,
                    "no_action",
                    "BTCUSDT",
                    "HOLD",
                    "BTC震荡",
                    Decimal("0.3000"),
                    margin_amount=Decimal("0E-8"),
                ),
            ),
        ),
    )

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Daily net PnL: 0" in result.stdout
    assert "Total isolated margin: 0" in result.stdout
    assert "0E-8" not in result.stdout
    assert "0.3000" not in result.stdout
    assert "0.3" in result.stdout


def test_live_futures_ai_trade_batch_once_command_keeps_reason_chinese(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_futures_ai_trader_enabled: true",
                "live_futures_confirm_production: true",
            ]
        ),
        encoding="utf-8",
    )
    monkeypatch.setattr(
        "binance_quant.cli.run_live_futures_ai_trade_batch_once",
        lambda settings: FuturesAiBatchResult(
            status="no_action",
            reason="无合约动作",
            max_actions=5,
            prediction_count=2,
            executed_count=0,
            daily_realized_pnl=Decimal("0"),
            total_isolated_margin=Decimal("0"),
            results=(
                FuturesExecutionResult(
                    37,
                    "rejected",
                    "BNBUSDT",
                    "CLOSE",
                    "AI: BNB持仓风险升高; 风控: 交易规则与交易对不匹配",
                    Decimal("0.75"),
                    close_percent=100,
                ),
                FuturesExecutionResult(
                    35,
                    "rejected",
                    "BTCUSDT",
                    "OPEN_SHORT",
                    "AI: BTC短线下行动能增强; 风控: 名义金额低于最小下单金额",
                    Decimal("0.74"),
                    leverage=2,
                    margin_amount=Decimal("5"),
                ),
            ),
        ),
    )

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "交易规则与交易对不匹配" in result.stdout
    assert "名义金额低于最小下单金额" in result.stdout
    assert "metadata symbol mismatch" not in result.stdout
    assert "notional below min notional" not in result.stdout


def test_live_futures_ai_trade_batch_once_command_sends_email_notification(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_futures_ai_trader_enabled: true",
                "live_futures_confirm_production: true",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_login: 19176654177@163.com",
                "email_sender_password: secret-token",
                "email_recipient: xia_qingbo@163.com",
            ]
        ),
        encoding="utf-8",
    )
    batch_result = FuturesAiBatchResult(
        status="completed",
        reason="AI合约批量交易完成",
        max_actions=5,
        prediction_count=1,
        executed_count=1,
        daily_realized_pnl=Decimal("0"),
        total_isolated_margin=Decimal("5"),
        results=(FuturesExecutionResult(1, "protected", "BTCUSDT", "OPEN_LONG", "protected", Decimal("0.88")),),
    )
    sent = []

    monkeypatch.setattr("binance_quant.cli.run_live_futures_ai_trade_batch_once", lambda settings: batch_result)
    monkeypatch.setattr("binance_quant.cli.send_futures_ai_trade_batch_email", lambda settings, result: sent.append((settings, result)))

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    [(settings, sent_result)] = sent
    assert settings.email_notifications_enabled is True
    assert sent_result is batch_result


def test_live_futures_ai_trade_batch_once_command_exits_nonzero_when_email_fails_without_leaking_secret(
    tmp_path,
    monkeypatch,
):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_futures_ai_trader_enabled: true",
                "live_futures_confirm_production: true",
                "email_notifications_enabled: true",
                "email_smtp_server: smtp.163.com",
                "email_login: 19176654177@163.com",
                "email_sender_password: secret-token",
                "email_recipient: xia_qingbo@163.com",
            ]
        ),
        encoding="utf-8",
    )
    batch_result = FuturesAiBatchResult(
        status="completed",
        reason="AI合约批量交易完成",
        max_actions=5,
        prediction_count=1,
        executed_count=1,
        daily_realized_pnl=Decimal("0"),
        total_isolated_margin=Decimal("5"),
        results=(FuturesExecutionResult(1, "protected", "BTCUSDT", "OPEN_LONG", "protected", Decimal("0.88")),),
    )

    monkeypatch.setattr("binance_quant.cli.run_live_futures_ai_trade_batch_once", lambda settings: batch_result)

    def fail_email(settings, result):
        raise RuntimeError("email notification failed")

    monkeypatch.setattr("binance_quant.cli.send_futures_ai_trade_batch_email", fail_email)

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Status: completed" in result.stdout
    assert "Email notification failed" in result.output
    assert "secret-token" not in result.output


def test_live_futures_ai_trade_batch_once_command_exits_nonzero_for_failure(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "mode: live\napi_key: key\napi_secret: secret\nlive_futures_ai_trader_enabled: true\nlive_futures_confirm_production: true\n",
        encoding="utf-8",
    )
    monkeypatch.setattr(
        "binance_quant.cli.run_live_futures_ai_trade_batch_once",
        lambda settings: FuturesAiBatchResult(
            status="completed",
            reason="failure",
            max_actions=5,
            prediction_count=1,
            executed_count=1,
            daily_realized_pnl=Decimal("0"),
            total_isolated_margin=Decimal("5"),
            results=(FuturesExecutionResult(1, "rollback_failed", "BTCUSDT", "OPEN_LONG", "failed", Decimal("0.88")),),
        ),
    )

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 1
    assert "rollback_failed" in result.stdout


def test_live_futures_ai_trade_batch_once_command_sanitizes_http_errors(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "mode: live\napi_key: key\napi_secret: secret\nlive_futures_ai_trader_enabled: true\nlive_futures_confirm_production: true\n",
        encoding="utf-8",
    )

    def fail(settings):
        request = httpx.Request(
            "POST",
            "https://fapi.binance.com/fapi/v1/order?timestamp=1&token=secret-token&signature=secret",
        )
        response = httpx.Response(400, request=request)
        raise httpx.HTTPStatusError("signed futures request failed", request=request, response=response)

    monkeypatch.setattr("binance_quant.cli.run_live_futures_ai_trade_batch_once", fail)

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code != 0
    assert "Binance API request failed" in result.output
    assert "signature=" not in result.output
    assert "token=" not in result.output
    assert "secret-token" not in result.output
    assert "/fapi/v1/order?" not in result.output
    assert "timestamp=1" not in result.output
