#!/usr/bin/env -S uv run
# ruff: noqa: C901, TRY300
"""
Block Trades Workflow Integration Test - Standalone Script

USAGE:
    # First generate test accounts (required once):
    uv run generate_test_keys.py

    # Then run the integration test (uv will auto-install dependencies):
    uv run tests/integration/test_block_trades_workflow.py

    # Or run from the project root with installed dependencies:
    python tests/integration/test_block_trades_workflow.py

PREREQUISITES:
    - test_accounts.json file (generated by generate_test_keys.py)
    - Account funding on Paradex testnet (for actual trade execution)
    - Network connection to Paradex testnet API

WORKFLOW OVERVIEW:
    1. Load test accounts from test_accounts.json
    2. Create Paradex clients for each account
    3. Test account authentication and check funding
    4. Create a block trade with multiple markets
    5. Submit offers from signer accounts
    6. Execute the block trade with selected offers
    7. Monitor execution status changes
    8. Test cancellation operations
    9. Demonstrate error handling

This test creates multiple accounts, onboards them, and demonstrates
the full offer-based block trades workflow against Paradex testnet.

**Key Features:**
- Creates 3 test accounts (initiator + 2 signers) with real Ethereum private keys
- Demonstrates complete authentication and account onboarding
- Shows proper BlockTradeRequest structure with constraints
- Implements offer submission from required signers
- Includes execution, listing, and cancellation operations
- Handles enum serialization properly (SignatureType, OrderSide, etc.)
- Provides comprehensive error handling

**Technical Notes:**
- Uses model_dump(mode="json") for proper enum serialization
- Bypasses API client methods to handle serialization issues
- Includes debug information for troubleshooting
- Tests against Paradex testnet environment
"""

import json
import logging
import time
from decimal import Decimal
from typing import List

from pydantic import TypeAdapter

from paradex_py import Paradex
from paradex_py.api.generated.requests import (
    BlockExecuteRequest,
    BlockOfferInfo,
    BlockOfferRequest,
    BlockTradeConstraints,
    BlockTradeInfo,
    BlockTradeRequest,
)
from paradex_py.api.generated.responses import (
    ApiError,
    BlockTradeOrder,
    BlockTradeSignature,
    MarketResp,
    MarketSummaryResp,
    OrderInstruction,
    OrderSide,
    OrderType,
    SignatureType,
)
from paradex_py.common.order import Order
from paradex_py.common.order import OrderSide as CommonOrderSide
from paradex_py.common.order import OrderType as CommonOrderType
from paradex_py.message.block_trades import BlockTrade, Trade

# Set up logging with better console formatting
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)-8s | %(message)s", datefmt="%H:%M:%S")
logger = logging.getLogger(__name__)

# Console formatting helpers
class Colors:
    """ANSI color codes for console output"""

    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    BLUE = "\033[94m"
    CYAN = "\033[96m"
    BOLD = "\033[1m"
    END = "\033[0m"


def log_step(step_name: str):
    """Log a major test step with clear formatting"""
    logger.info(f"\n{Colors.BOLD}{Colors.BLUE}{'='*60}{Colors.END}")
    logger.info(f"{Colors.BOLD}{Colors.BLUE}🔹 {step_name}{Colors.END}")
    logger.info(f"{Colors.BOLD}{Colors.BLUE}{'='*60}{Colors.END}")


def log_success(message: str):
    """Log success with green formatting"""
    logger.info(f"{Colors.GREEN}✅ {message}{Colors.END}")


def log_warning(message: str):
    """Log warning with yellow formatting"""
    logger.warning(f"{Colors.YELLOW}⚠️  {message}{Colors.END}")


def log_error(message: str):
    """Log error with red formatting"""
    logger.error(f"{Colors.RED}❌ {message}{Colors.END}")


def log_info(message: str):
    """Log info with cyan formatting"""
    logger.info(f"{Colors.CYAN}i️  {message}{Colors.END}")


# Constants
TESTNET_ENVIRONMENT = "nightly"
MINIMUM_ACCOUNT_VALUE = Decimal("1000.0")  # Minimum USDC needed for testing
MINIMUM_FREE_COLLATERAL = Decimal("500.0")  # Minimum free collateral needed


# Utility functions for block trade signing
def create_and_sign_order(
    client, market: str, side: str, price: Decimal, size: Decimal, client_id: str, timestamp: int
) -> tuple[Order, str]:
    """Create and sign an order, returning both the Order object and signature."""
    order_side = CommonOrderSide.Buy if side.upper() == "BUY" else CommonOrderSide.Sell

    order = Order(
        market=market,
        order_type=CommonOrderType.Limit,
        order_side=order_side,
        size=size,
        limit_price=price,
        client_id=client_id,
        signature_timestamp=timestamp,
        instruction="GTC",
    )

    signature = client.account.sign_order(order)
    return order, signature


def create_block_trade_order(
    market: str, side: str, price: str, size: str, signature: str, client_id: str, timestamp: int
) -> BlockTradeOrder:
    """Create a BlockTradeOrder with all required fields."""
    order_side = OrderSide.order_side_buy if side.upper() == "BUY" else OrderSide.order_side_sell

    return BlockTradeOrder(
        client_id=client_id,
        flags=None,
        instruction=OrderInstruction.order_instruction_gtc,
        market=market,
        on_behalf_of_account=None,
        price=price,
        recv_window=None,
        side=order_side,
        signature=signature,
        signature_timestamp=timestamp,
        signed_impact_price=None,
        size=size,
        stp=None,
        trigger_price=None,
        type=OrderType.order_type_limit,
    )


def sign_block_trade_request(client, request: BlockTradeRequest, signer_account: str) -> BlockTradeRequest:
    """
    Sign a BlockTradeRequest by creating the necessary BlockTrade object and adding signatures.
    Supports multiple markets in the request.
    """
    # Create list of Trade objects from the request
    trades = []

    for market_symbol, trade_info in request.trades.items():
        # Extract order details from the trade info
        maker_order_data = trade_info.maker_order

        # Create Order object for signing
        maker_order = Order(
            market=market_symbol,
            order_type=CommonOrderType.Limit,
            order_side=CommonOrderSide.Buy
            if maker_order_data and maker_order_data.side == OrderSide.order_side_buy
            else CommonOrderSide.Sell,
            size=Decimal(str(trade_info.size)) if trade_info.size else Decimal("0"),
            limit_price=Decimal(str(trade_info.price)) if trade_info.price else Decimal("0"),
            client_id=maker_order_data.client_id if maker_order_data and maker_order_data.client_id else "",
            signature_timestamp=maker_order_data.signature_timestamp
            if maker_order_data and maker_order_data.signature_timestamp
            else 0,
            instruction="GTC",
        )

        # For simplicity, use maker order for both maker and taker
        trade = Trade(
            price=Decimal(str(trade_info.price)) if trade_info.price else Decimal("0"),
            size=Decimal(str(trade_info.size)) if trade_info.size else Decimal("0"),
            maker_order=maker_order,
            taker_order=maker_order,
        )
        trades.append(trade)

    # Create BlockTrade object
    block_trade = BlockTrade(version="1.0", trades=trades)

    # Sign the block trade
    signature = client.account.sign_block_trade(block_trade)

    # Create BlockTradeSignature
    current_time = int(time.time() * 1000)
    block_signature = BlockTradeSignature(
        nonce=f"bt_sig_{current_time}",
        signature_data=signature,
        signature_expiration=current_time + (5 * 60 * 1000),  # 5 minutes
        signature_timestamp=current_time,
        signature_type=SignatureType.starknet,
        signer_account=signer_account,
    )

    # Add signature to request
    request.signatures[signer_account] = block_signature

    return request


def sign_block_offer_request(client, request: BlockOfferRequest, signer_account: str) -> BlockOfferRequest:
    """
    Sign a BlockOfferRequest by creating the necessary BlockTrade object.
    Supports multiple markets in the request.
    """
    # Create list of Trade objects from the request
    trades = []

    for market_symbol, offer_info in request.trades.items():
        # Extract order details from the offer info
        offerer_order_data = offer_info.offerer_order

        # Create Order object for signing
        offerer_order = Order(
            market=market_symbol,
            order_type=CommonOrderType.Limit,
            order_side=CommonOrderSide.Buy
            if offerer_order_data.side == OrderSide.order_side_buy
            else CommonOrderSide.Sell,
            size=Decimal(str(offer_info.size)) if offer_info.size else Decimal("0"),
            limit_price=Decimal(str(offer_info.price)) if offer_info.price else Decimal("0"),
            client_id=offerer_order_data.client_id if offerer_order_data and offerer_order_data.client_id else "",
            signature_timestamp=offerer_order_data.signature_timestamp,
            instruction="GTC",
        )

        # For offers, use the same order for both maker and taker
        trade = Trade(
            price=Decimal(offer_info.price),
            size=Decimal(offer_info.size),
            maker_order=offerer_order,
            taker_order=offerer_order,
        )
        trades.append(trade)

    # Create BlockTrade object
    block_trade = BlockTrade(version="1.0", trades=trades)

    # Sign the block offer
    signature = client.account.sign_block_offer(block_trade)

    # Create BlockTradeSignature
    current_time = int(time.time() * 1000)
    offer_signature = BlockTradeSignature(
        nonce=f"offer_sig_{current_time}",
        signature_data=signature,
        signature_expiration=current_time + (5 * 60 * 1000),  # 5 minutes
        signature_timestamp=current_time,
        signature_type=SignatureType.starknet,
        signer_account=signer_account,
    )

    # Set signature on request
    request.signature = offer_signature

    return request


def load_test_accounts():
    """Load test accounts from JSON file."""
    try:
        with open("test_accounts.json") as f:
            accounts = json.load(f)
        logger.info(f"Loaded {len(accounts)} test accounts")
        return accounts
    except FileNotFoundError as e:
        logger.error("test_accounts.json not found. Please run generate_test_keys.py first.")
        raise e


def create_paradex_clients(accounts):
    """Create Paradex clients for all test accounts."""
    clients = []
    for i, account_data in enumerate(accounts, 1):
        # Create Paradex client using the constructor pattern from the codebase
        client = Paradex(
            env=TESTNET_ENVIRONMENT,
            l1_address=account_data["l1_address"],
            l1_private_key=account_data["l1_private_key"],
        )
        clients.append(client)
        logger.info(f"Created client for account_{i}: {account_data['l1_address']}")
    return clients


def check_account_balance(client, account_name):
    """Check account balance and return whether it's sufficient for trading."""
    try:
        summary = client.api_client.fetch_account_summary()
        logger.info(f"{account_name} summary: {summary}")

        account_value = Decimal(summary.account_value)
        free_collateral = Decimal(summary.free_collateral)

        has_sufficient_funds = account_value >= MINIMUM_ACCOUNT_VALUE and free_collateral >= MINIMUM_FREE_COLLATERAL

        if has_sufficient_funds:
            logger.info(
                f"✅ {account_name} has sufficient funds (Account Value: {account_value}, Free Collateral:"
                f" {free_collateral})"
            )
        else:
            logger.warning(
                f"⚠️ {account_name} has insufficient funds (Account Value: {account_value}, Free Collateral:"
                f" {free_collateral})"
            )
            logger.warning(
                f"   Required: Account Value >= {MINIMUM_ACCOUNT_VALUE}, Free Collateral >= {MINIMUM_FREE_COLLATERAL}"
            )

        return has_sufficient_funds, summary

    except Exception as e:
        logger.error(f"Failed to check {account_name} balance: {e}")
        return False, None


def test_account_creation_and_onboarding(clients):
    """Test account creation and onboarding process."""
    log_step("Account Creation and Onboarding")

    all_accounts_funded = True
    account_summaries = []

    for i, client in enumerate(clients, 1):
        account_name = f"Account {i}"
        has_funds, summary = check_account_balance(client, account_name)
        account_summaries.append((account_name, has_funds, summary))

        if has_funds:
            log_success(f"{account_name} authenticated successfully")
        else:
            log_warning(f"{account_name} authenticated but needs funding")
            all_accounts_funded = False

    return all_accounts_funded, account_summaries


def fetch_multiple_markets_data(client, num_markets=3):
    """Fetch market data for creating test block trades with multiple markets."""
    logger.info(f"Fetching data for {num_markets} markets...")

    try:
        # Fetch available markets
        markets_response = client.api_client.fetch_markets()

        # Check for API error
        if "error" in markets_response:
            error = ApiError.model_validate(markets_response)
            logger.error(f"API Error {error.error}: {error.message}")
            return []

        logger.info(f"Available markets: {len(markets_response.get('results', []))} markets")

        # Parse all markets using TypeAdapter
        if markets_response.get("results"):
            adapter = TypeAdapter(List[MarketResp])
            all_markets = adapter.validate_python(markets_response["results"])
        else:
            logger.error("No markets found in response")
            return []

        # Find perpetual markets
        perp_markets = [m for m in all_markets if m.symbol and "-PERP" in m.symbol]
        if not perp_markets:
            logger.error("No perpetual markets found")
            return []

        # Prioritize common markets: ETH, BTC, SOL
        preferred_symbols = ["ETH-USD-PERP", "BTC-USD-PERP", "SOL-USD-PERP"]
        selected_markets: List[MarketResp] = []

        # First, try to get preferred markets
        for symbol in preferred_symbols:
            market = next((m for m in perp_markets if m.symbol == symbol), None)
            if market and len(selected_markets) < num_markets:
                selected_markets.append(market)

        # Fill remaining slots with other available markets
        for market in perp_markets:
            if len(selected_markets) >= num_markets:
                break
            if market not in selected_markets:
                selected_markets.append(market)

        # Fetch market summaries for current prices
        markets_with_prices = []
        for market in selected_markets[:num_markets]:
            try:
                market_symbol = market.symbol
                if not market_symbol:
                    continue

                summary_response = client.api_client.fetch_markets_summary({"market": market_symbol})

                # Check for API error
                if "error" in summary_response:
                    error = ApiError.model_validate(summary_response)
                    logger.warning(f"API Error for {market_symbol}: {error.error} - {error.message}")
                    continue

                if summary_response and summary_response.get("results"):
                    # Parse market summaries using TypeAdapter
                    adapter = TypeAdapter(List[MarketSummaryResp])
                    summaries = adapter.validate_python(summary_response["results"])

                    if summaries:
                        summary = summaries[0]
                        # Get price from MarketSummaryResp model fields
                        current_price = float(summary.mark_price or summary.underlying_price or "0")
                        if current_price > 0:
                            markets_with_prices.append((market, current_price))
                            logger.info(f"Using market {market_symbol}, current price: {current_price}")
                        else:
                            logger.warning(f"No valid price for {market_symbol}")
                else:
                    logger.warning(f"No results in market summary for {market_symbol}")
            except Exception as e:
                logger.warning(f"Failed to fetch/parse summary for {market.symbol}: {e}")
                continue

        logger.info(f"Successfully fetched data for {len(markets_with_prices)} markets")
        return markets_with_prices

    except Exception as e:
        logger.error(f"Error fetching market data: {e}")
        return []


def test_create_block_trade(client, account_summaries):
    """Test creating a block trade with multiple markets."""
    log_step("Block Trade Creation")

    # Check if initiator account has sufficient funds
    initiator_funded = account_summaries[0][1] if account_summaries else False
    initiator_account = account_summaries[0][2].account if account_summaries and account_summaries[0][2] else None

    if not initiator_funded or not initiator_account:
        log_warning("Initiator account lacks sufficient funds or info - block trade creation will fail")
        return None

    # Fetch market data for 1-3 markets
    import random

    num_markets = random.randint(1, 3)  # Randomly choose 1-3 markets
    markets_data = fetch_multiple_markets_data(client, num_markets)

    if not markets_data:
        logger.error("Failed to fetch market data")
        return None

    log_info(f"Creating block trade with {len(markets_data)} markets")

    # Create trades dictionary for each market
    trades = {}
    current_time = int(time.time() * 1000)

    for i, (market, current_price) in enumerate(markets_data):
        market_symbol = market.symbol
        if not market_symbol:
            continue

        # Use the proper MarketResp fields with safe defaults
        tick_size = float(market.price_tick_size or "0.01")
        step_size = float(market.order_size_increment or "0.001")
        min_notional = float(market.min_notional or "1.0")

        # Calculate min_order_size from min_notional if needed
        # For futures: min_notional = size * mark_price, so min_size = min_notional / mark_price
        min_order_size = max(step_size, min_notional / current_price)

        # Use alternating sides for variety
        side = "BUY" if i % 2 == 0 else "SELL"

        # Use the actual market constraints (no hardcoded overrides needed)
        actual_step_size = step_size
        actual_tick_size = tick_size

        # Calculate price based on side using actual_tick_size
        if side == "BUY":
            order_price = round(current_price * 0.99 / actual_tick_size) * actual_tick_size  # Slightly below market
        else:
            order_price = round(current_price * 1.01 / actual_tick_size) * actual_tick_size  # Slightly above market

        # Set constraints around the order price using actual_tick_size
        min_price = round(order_price * 0.95 / actual_tick_size) * actual_tick_size
        max_price = round(order_price * 1.05 / actual_tick_size) * actual_tick_size

        # Format prices to ensure proper decimal places
        price_decimals = len(str(actual_tick_size).split(".")[-1]) if "." in str(actual_tick_size) else 0
        order_price = round(order_price, price_decimals)
        min_price = round(min_price, price_decimals)
        max_price = round(max_price, price_decimals)

        # Ensure order_size is at least the minimum and is a multiple of actual_step_size
        target_size = 0.1
        order_size = max(min_order_size, target_size)
        order_size = round(order_size / actual_step_size) * actual_step_size

        size_decimals = max(len(str(actual_step_size).split(".")[-1]) if "." in str(actual_step_size) else 0, 1)
        order_size = round(order_size, size_decimals)

        # Calculate constraints that also conform to actual_step_size
        max_size_raw = order_size * 5
        min_size_raw = max(min_order_size, order_size * 0.5)

        # Round to actual_step_size multiples
        max_size = round(max_size_raw / actual_step_size) * actual_step_size
        min_size = round(min_size_raw / actual_step_size) * actual_step_size

        # Ensure min_size is at least min_order_size and conforms to actual_step_size
        if min_size < min_order_size:
            min_size = round(min_order_size / actual_step_size) * actual_step_size

        # Additional safeguard: ensure min_size is never zero or negative
        if min_size <= 0:
            min_size = max(min_order_size, actual_step_size)

        max_size = round(max_size, size_decimals)
        min_size = round(min_size, size_decimals)

        # Create constraints for this market
        constraints = BlockTradeConstraints(
            min_price=str(min_price), max_price=str(max_price), min_size=str(min_size), max_size=str(max_size)
        )

        # Create and sign the maker order
        try:
            _, order_signature = create_and_sign_order(
                client,
                market_symbol,
                side,
                Decimal(str(order_price)),
                Decimal(str(order_size)),
                f"client_{current_time}_{i}",
                current_time - 1000,
            )

            # Create maker order with real signature
            maker_order = create_block_trade_order(
                market_symbol,
                side,
                str(order_price),
                str(order_size),
                order_signature,
                f"client_{current_time}_{i}",
                current_time - 1000,
            )
        except Exception as e:
            logger.error(f"Failed to sign maker order for {market_symbol}: {e}")
            continue

        # Create block trade info for this market
        trade_info = BlockTradeInfo(
            maker_order=maker_order,
            price=str(order_price),
            size=str(order_size),
            taker_order=None,
            trade_constraints=constraints,
        )

        trades[market_symbol] = trade_info
        log_info(f"Added {side} order for {market_symbol}: {order_size} @ {order_price}")

    if not trades:
        logger.error("Failed to create any valid trades")
        return None

    # Get required signers - start with the initiator account
    required_signers = [initiator_account]
    log_info(f"Adding initiator as required signer: {initiator_account}")

    # Add other test accounts as additional signers
    for i, (_, funded, summary) in enumerate(account_summaries[1:3], 1):
        if funded and summary and summary.account:
            required_signers.append(summary.account)
            log_info(f"Adding required signer {i}: {summary.account}")

    if len(required_signers) == 1:
        log_warning("Only initiator account available as signer - block trade will only require initiator signature")

    # Create unsigned block trade request with all markets
    request = BlockTradeRequest(
        nonce=f"block_trade_{current_time}",
        required_signers=required_signers,
        signatures={},  # Will be filled by utility function
        trades=trades,
    )

    # Sign the block trade request
    try:
        request = sign_block_trade_request(client, request, initiator_account)
    except Exception as e:
        logger.error(f"Failed to sign block trade: {e}")
        return None
    # Convert to JSON for API call
    request_data = request.model_dump(mode="json")
    # Patch signature_type for all signatures to be string
    for sig in request_data["signatures"].values():
        sig["signature_type"] = "STARKNET"

    # Log summary of the request
    logger.info("Block trade request summary:")
    logger.info(f"  Markets: {list(trades.keys())}")
    for market_symbol, trade_info in trades.items():
        maker_order = trade_info.maker_order
        if maker_order and maker_order.side:
            side = maker_order.side.value if hasattr(maker_order.side, "value") else maker_order.side
        else:
            side = "UNKNOWN"
        logger.info(f"    {market_symbol}: {side} {maker_order.size} @ {maker_order.price}")
    logger.info(f"  Required signers: {required_signers}")
    logger.info("  Expiration: 5 minutes from creation")

    try:
        # Make API call
        response = client.api_client.create_block_trade(request)

        # Validate response and warn about missing data
        if response.trades:
            for market, trade in response.trades.items():
                if trade.maker_order and not trade.maker_order.signature:
                    log_warning(f"Block trade maker order missing signature for {market}")

        log_success(f"Block trade created successfully: {response.block_id or 'Unknown ID'}")
        # Return both response and market data for use in subsequent tests
        first_market = markets_data[0][0] if markets_data else None
        return response, first_market
    except Exception as e:
        expected_errors = ["VALIDATION_ERROR", "Account is not active", "invalid request"]
        if any(err in str(e) for err in expected_errors):
            if not initiator_funded:
                logger.warning(f"⚠️ Block trade creation failed as expected (account needs funding): {e}")
            else:
                logger.warning(f"⚠️ Block trade creation failed: {e}")
        else:
            logger.error(f"❌ Unexpected error in block trade creation: {e}")
        return None


def test_submit_offers(
    clients,
    block_trade_id,
    account_summaries,
    market_symbol,
    base_price,
    market_tick_size,
    market_step_size,
    market_min_notional,
):
    """Test submitting offers to a block trade."""
    log_step("Offer Submission")

    # Note: market_symbol, market_tick_size, market_step_size, market_min_notional are now unused
    # as we fetch market data dynamically for all markets in the block trade
    _ = market_symbol, market_tick_size, market_step_size, market_min_notional

    # First, get the block trade details to understand all markets involved
    try:
        block_trade_response = clients[0].api_client.get_block_trade(block_trade_id)
        all_markets = {}
        if hasattr(block_trade_response, "trades") and block_trade_response.trades:
            for market, trade_detail in block_trade_response.trades.items():
                all_markets[market] = {
                    "price": getattr(trade_detail, "price", None),
                    "size": getattr(trade_detail, "size", None),
                    "side": getattr(getattr(trade_detail, "maker_order", None), "side", None),
                }
        log_info(f"Block trade contains {len(all_markets)} markets: {list(all_markets.keys())}")
    except Exception as e:
        log_error(f"Failed to fetch block trade details: {e}")
        return

    # Use signers (accounts 2 and 3) for offers
    signer_clients = clients[1:3]
    signer_summaries = (
        account_summaries[1:3] if account_summaries else [(f"Account {i}", False, None) for i in range(2, 4)]
    )

    for i, (client, (account_name, has_funds, summary)) in enumerate(zip(signer_clients, signer_summaries), 1):
        if not has_funds:
            logger.warning(f"⚠️ {account_name} lacks sufficient funds - offer may fail")
            continue

        signer_account = summary.account if summary else None
        if not signer_account:
            logger.error(f"No account information for {account_name}")
            continue

        current_time = int(time.time() * 1000)

        # Create trades for ALL markets in the block trade
        offer_trades = {}

        for market_name, market_info in all_markets.items():
            # Get market data for this specific market
            try:
                markets_response = client.api_client.fetch_markets()
                market_data = None
                for market_dict in markets_response.get("results", []):
                    if market_dict.get("symbol") == market_name:
                        market_data = market_dict
                        break

                if not market_data:
                    log_error(f"Market data not found for {market_name}")
                    continue

                current_tick_size = float(market_data.get("price_tick_size", "0.01"))
                current_step_size = float(market_data.get("order_size_increment", "0.001"))
                current_min_notional = float(market_data.get("min_notional", "10.0"))

            except Exception as e:
                log_error(f"Failed to fetch market data for {market_name}: {e}")
                continue

            # Use the original maker's price as base for competitive pricing
            original_price = float(market_info.get("price", base_price))

            # Create competitive offer price (slightly better than original)
            # Use Decimal for precise calculations to avoid floating point errors
            base_price_decimal = Decimal(str(original_price))
            tick_size_decimal = Decimal(str(current_tick_size))
            step_size_decimal = Decimal(str(current_step_size))
            min_notional_decimal = Decimal(str(current_min_notional))

            # Calculate raw price with 2% better price for sell order
            raw_price_decimal = base_price_decimal * Decimal("0.98")

            # Round to nearest tick size using Decimal arithmetic
            ticks = (raw_price_decimal / tick_size_decimal).quantize(Decimal("1"), rounding="ROUND_HALF_UP")
            offer_price_decimal = ticks * tick_size_decimal

            # Calculate minimum size needed to meet min_notional requirement
            min_size_for_notional_decimal = min_notional_decimal / offer_price_decimal

            # Ensure offer size meets both step size and min_notional requirements
            raw_size_decimal = max(Decimal("0.05"), min_size_for_notional_decimal)

            # Round to nearest step size using Decimal arithmetic
            steps = (raw_size_decimal / step_size_decimal).quantize(Decimal("1"), rounding="ROUND_HALF_UP")
            offer_size_decimal = steps * step_size_decimal

            # Verify the final size meets min_notional (in case rounding affected it)
            actual_notional_decimal = offer_size_decimal * offer_price_decimal
            if actual_notional_decimal < min_notional_decimal:
                # Add one more step size increment to ensure we meet min_notional
                offer_size_decimal += step_size_decimal
                actual_notional_decimal = offer_size_decimal * offer_price_decimal

            # Convert back to float for API calls
            offer_price = float(offer_price_decimal)
            offer_size = float(offer_size_decimal)

            # Determine offer side - must be opposite to maker side
            maker_side = market_info.get("side")
            maker_side_str = str(maker_side)  # Convert to string for comparison

            if maker_side_str in ["BUY", "order_side_buy"] or maker_side == OrderSide.order_side_buy:
                offer_side = "SELL"
            elif maker_side_str in ["SELL", "order_side_sell"] or maker_side == OrderSide.order_side_sell:
                offer_side = "BUY"
            else:
                log_error(f"Unknown maker side '{maker_side}' for {market_name}, defaulting to SELL")
                offer_side = "SELL"

            log_info(f"Market {market_name}: Maker {maker_side} → Offer {offer_side}")

            # Create and sign the offerer order for this market
            try:
                _, offerer_order_signature = create_and_sign_order(
                    client,
                    market_name,
                    offer_side,
                    Decimal(str(offer_price)),
                    Decimal(str(offer_size)),
                    f"offer_client_{current_time}_{i}_{market_name}",
                    current_time - 1000,
                )

                # Create offerer order with real signature
                offerer_order = create_block_trade_order(
                    market_name,
                    offer_side,
                    str(offer_price),
                    str(offer_size),
                    offerer_order_signature,
                    f"offer_client_{current_time}_{i}_{market_name}",
                    current_time - 1000,
                )

                # Create offer info for this market
                offer_info = BlockOfferInfo(offerer_order=offerer_order, price=str(offer_price), size=str(offer_size))
                offer_trades[market_name] = offer_info

                log_info(f"Created offer for {market_name}: {offer_side} {offer_size} @ {offer_price}")

            except Exception as e:
                logger.error(f"Failed to create offer for {market_name}: {e}")
                continue

        if not offer_trades:
            log_error(f"Failed to create any valid trades for {account_name}")
            continue

        # Create a dummy signature placeholder (will be replaced by utility function)
        dummy_signature = BlockTradeSignature(
            nonce=f"temp_{current_time}",
            signature_data="temp",
            signature_expiration=current_time + 300000,
            signature_timestamp=current_time,
            signature_type=SignatureType.starknet,
            signer_account=signer_account,
        )

        # Create unsigned offer request with ALL markets
        request = BlockOfferRequest(
            nonce=f"offer_{current_time}_{i}",
            offering_account=signer_account,
            signature=dummy_signature,  # Will be replaced by utility function
            trades=offer_trades,
        )

        # Sign the offer request
        try:
            request = sign_block_offer_request(client, request, signer_account)
        except Exception as e:
            logger.error(f"Failed to sign offer for {account_name}: {e}")
            continue
        # Convert to JSON for API call
        request_data = request.model_dump(mode="json")
        # Patch signature_type to string
        request_data["signature"]["signature_type"] = "STARKNET"
        # Log offer summary instead of full payload
        markets_summary = ", ".join(
            [f"{market} {info.offerer_order.side} {info.size} @ {info.price}" for market, info in offer_trades.items()]
        )
        log_info(f"Submitting offer for {account_name}: {markets_summary}")

        try:
            # Make API call
            response = client.api_client.create_block_trade_offer(block_trade_id, request)
            offer_id = response.block_id or "Unknown"
            log_success(f"Offer {i} submitted successfully: {offer_id}")
        except Exception as e:
            expected_errors = ["VALIDATION_ERROR", "Offering account does not match", "Account is not active"]
            if any(err in str(e) for err in expected_errors):
                if not has_funds:
                    logger.warning(f"⚠️ Offer {i} submission failed as expected (account needs funding): {e}")
                else:
                    logger.warning(f"⚠️ Offer {i} submission failed: {e}")
            else:
                logger.error(f"❌ Unexpected error in offer {i} submission: {e}")


def test_list_offers(client, block_trade_id):
    """Test listing offers for a block trade."""
    logger.info("Testing offer listing...")

    try:
        response = client.api_client.get_block_trade_offers(block_trade_id)
        # Log offers summary instead of full response
        results = response.results or []
        if results:
            log_success(f"Listed {len(results)} offers")
            for i, offer in enumerate(results[:3], 1):  # Show max 3
                offer_id = offer.get("block_id") or offer.get("id", "Unknown")
                status = offer.get("status", "Unknown")
                log_info(f"  Offer {i}: {offer_id} (Status: {status})")
        else:
            log_success("Listed offers (empty)")
        return response
    except Exception as e:
        expected_errors = ["NOT_FOUND", "Block not found", "no offers available"]
        if any(err in str(e) for err in expected_errors):
            logger.warning(f"⚠️ Listing offers failed: {e}")
        else:
            logger.error(f"❌ Unexpected error in listing offers: {e}")
        return None


def poll_status_for_entities(client, block_ids, max_checks=30, interval=3, timeout_seconds=None):
    """
    Poll status for multiple block trades or offers using get_block_trade.

    Args:
        client: API client instance
        block_ids: List of block trade/offer IDs to monitor
        max_checks: Maximum number of polling iterations
        interval: Seconds between checks
        timeout_seconds: Optional absolute timeout (overrides max_checks)

    Returns:
        Dict mapping block_id -> final status, list of entities that reached final state
    """
    import time

    if not block_ids:
        log_info("No block IDs provided for polling")
        return {}, []

    log_info(f"🔍 Starting polling for {len(block_ids)} block trades: {block_ids}")

    # Calculate actual max_checks if timeout is specified
    if timeout_seconds:
        max_checks = min(max_checks, int(timeout_seconds / interval))
        log_info(f"Using timeout of {timeout_seconds}s ({max_checks} checks)")

    # Track entities still being monitored
    monitoring_list = list(block_ids)
    final_statuses = {}
    completed_entities = []

    start_time = time.time()

    for check in range(max_checks):
        if not monitoring_list:
            log_info("✅ All entities reached final state")
            break

        # Check timeout
        if timeout_seconds and (time.time() - start_time) >= timeout_seconds:
            log_info(f"⏰ Timeout reached ({timeout_seconds}s), stopping polling")
            break

        current_iteration = []
        entities_to_remove = []

        for block_id in monitoring_list:
            try:
                # Use get_block_trade for both block trades and offers
                response = client.api_client.get_block_trade(block_id)
                current_status = response.status

                # Store current status
                final_statuses[block_id] = current_status
                current_iteration.append(f"{block_id}:{current_status}")

                # Check if this entity has reached a final state
                if current_status in ["COMPLETED", "FAILED", "CANCELLED", "EXECUTED"]:
                    log_info(f"🎯 block trade {block_id} reached final status: {current_status}")
                    entities_to_remove.append(block_id)
                    completed_entities.append((block_id, current_status))

            except Exception as e:
                log_info(f"⚠️  Error checking block trade {block_id}: {e}")
                # Keep monitoring on errors
                current_iteration.append(f"{block_id}:ERROR")

        # Remove completed entities from monitoring list
        for block_id in entities_to_remove:
            monitoring_list.remove(block_id)

        # Log current status of all entities being monitored
        if current_iteration:
            status_summary = ", ".join(current_iteration)
            log_info(f"📊 Check {check+1}/{max_checks}: {status_summary}")
            if monitoring_list:
                log_info(f"   Still monitoring: {final_statuses}")

        # Sleep before next iteration if we haven't finished
        if monitoring_list and check < max_checks - 1:
            time.sleep(interval)

    # Final summary
    elapsed_time = time.time() - start_time
    log_info(f"🔚 Polling completed after {elapsed_time:.1f}s")
    log_info(f"   Final statuses: {final_statuses}")

    if monitoring_list:
        log_info(f"   ⚠️  {len(monitoring_list)} block trades did not reach final state: {monitoring_list}")

    return final_statuses, completed_entities


def test_execute_block_trade(client, block_trade_id, account_summaries, selected_offers=None):
    """Test executing a block trade."""
    logger.info("Testing block trade execution...")
    # Check if executor account has sufficient funds
    executor_funded = account_summaries[0][1] if account_summaries else False
    executor_account = account_summaries[0][2].account if account_summaries and account_summaries[0][2] else None
    if not executor_funded or not executor_account:
        logger.warning("⚠️ Executor account lacks sufficient funds or info - execution may fail")
        return None
    current_time = int(time.time() * 1000)
    expiration_time = current_time + (5 * 60 * 1000)  # 5 minutes
    # For execution, we need to sign each selected offer individually
    signatures = {}
    selected_offers = selected_offers or []
    if selected_offers:
        try:
            # Get the offers to extract their trade details for signing
            offers_response = client.api_client.get_block_trade_offers(block_trade_id)
            offers = offers_response.results or []
            if not offers:
                logger.warning("No offers found to execute")
                return None

            # Process each selected offer individually
            for offer_id in selected_offers:
                # Find the specific offer details
                offer = next((o for o in offers if o.get("id") == offer_id or o.get("block_id") == offer_id), None)
                if not offer:
                    logger.warning(f"Offer {offer_id} not found")
                    continue

                # Create individual trades for this specific offer
                offer_trades = []

                # Extract trade details from this offer
                for market_symbol, offer_info in offer.get("trades", {}).items():
                    offerer_order = offer_info.get("offerer_order", {})

                    # Create Order objects for this offer's trade
                    maker_order = Order(
                        market=market_symbol,
                        order_type=CommonOrderType.Limit,
                        order_side=CommonOrderSide.Buy if offerer_order.get("side") == "BUY" else CommonOrderSide.Sell,
                        size=Decimal(offer_info.get("size", "0")),
                        limit_price=Decimal(offer_info.get("price", "0")),
                        client_id=offerer_order.get("client_id", f"exec_{current_time}"),
                        signature_timestamp=current_time,
                        instruction="GTC",
                    )

                    taker_order = Order(
                        market=market_symbol,
                        order_type=CommonOrderType.Limit,
                        order_side=CommonOrderSide.Sell if offerer_order.get("side") == "BUY" else CommonOrderSide.Buy,
                        size=Decimal(offer_info.get("size", "0")),
                        limit_price=Decimal(offer_info.get("price", "0")),
                        client_id=f"taker_{current_time}",
                        signature_timestamp=current_time,
                        instruction="GTC",
                    )

                    # Create Trade object for this specific offer
                    trade = Trade(
                        price=Decimal(offer_info.get("price", "0")),
                        size=Decimal(offer_info.get("size", "0")),
                        maker_order=maker_order,
                        taker_order=taker_order,
                    )
                    offer_trades.append(trade)

                if offer_trades:
                    # Create BlockTrade for this specific offer
                    offer_block_trade = BlockTrade(version="1.0", trades=offer_trades)

                    # Sign this specific offer as the initiator
                    offer_signature_str = client.account.sign_block_trade(offer_block_trade)

                    # Create signature for this offer
                    offer_signature = BlockTradeSignature(
                        nonce=f"exec_offer_{offer_id}_{current_time}",
                        signature_data=offer_signature_str,
                        signature_expiration=expiration_time,
                        signature_timestamp=current_time,
                        signature_type=SignatureType.starknet,
                        signer_account=executor_account,
                    )

                    # Store signature with offer ID as key
                    signatures[offer_id] = offer_signature
                    log_info(f"Signed offer {offer_id} for execution")

        except Exception as e:
            logger.error(f"Failed to create execution signatures: {e}")
            return None

    # Create execute request
    request = BlockExecuteRequest(
        execution_nonce=f"execute_{current_time}", selected_offers=selected_offers, signatures=signatures
    )
    # Convert to JSON for API call
    request_data = request.model_dump(mode="json")
    for sig in request_data["signatures"].values():
        sig["signature_type"] = "STARKNET"
    log_info(f"Executing block trade with {len(selected_offers)} selected offers and {len(signatures)} signatures")

    try:
        # Make API call
        response = client.api_client.execute_block_trade(block_trade_id, request)
        logger.info(f"✅ Block trade executed successfully: {response}")
        return response
    except Exception as e:
        expected_errors = ["VALIDATION_ERROR", "Account is not active", "invalid request"]
        if any(err in str(e) for err in expected_errors):
            if not executor_funded:
                logger.warning(f"⚠️ Block trade execution failed as expected (account needs funding): {e}")
            else:
                logger.warning(f"⚠️ Block trade execution failed: {e}")
        else:
            logger.error(f"❌ Unexpected error in block trade execution: {e}")
        return None


def test_list_block_trades(client):
    """Test listing block trades."""
    logger.info("Testing block trade listing...")

    try:
        response = client.api_client.list_block_trades()
        results = response.results or []
        logger.info(f"✅ Successfully listed block trades: {len(results)} found")
        return response
    except Exception as e:
        logger.error(f"❌ Error in listing block trades: {e}")
        return None


def test_cancel_operations(client, block_trade_id, offer_ids):
    """Test canceling offers and block trades."""
    logger.info("Testing cancellation operations...")

    if not block_trade_id or block_trade_id == "test_block_trade_id":
        logger.warning("⚠️ Skipping cancellation tests - no valid block trade ID")
        return

    # Cancel offers if we have real offer IDs
    if offer_ids and len(offer_ids) > 0:
        for offer_id in offer_ids:
            try:
                response = client.api_client.cancel_block_trade_offer(block_trade_id, offer_id)
                logger.info(f"✅ Canceled offer {offer_id}: {response}")
            except Exception as e:
                expected_errors = [
                    "NOT_FOUND",
                    "INTERNAL_ERROR",
                    "Offer not found",
                    "Block trade not found",
                    "already executed",
                ]
                if any(err in str(e) for err in expected_errors):
                    logger.warning(f"⚠️ Canceling offer {offer_id} failed (expected): {e}")
                else:
                    logger.error(f"❌ Unexpected error canceling offer {offer_id}: {e}")

    # Cancel block trade
    try:
        response = client.api_client.cancel_block_trade(block_trade_id)
        logger.info(f"✅ Canceled block trade {block_trade_id}: {response}")
    except Exception as e:
        expected_errors = ["NOT_FOUND", "INTERNAL_ERROR", "Block trade not found", "already executed", "has offers"]
        if any(err in str(e) for err in expected_errors):
            logger.warning(f"⚠️ Canceling block trade {block_trade_id} failed (expected): {e}")
        else:
            logger.error(f"❌ Unexpected error canceling block trade {block_trade_id}: {e}")


def test_error_handling(client, valid_block_trade_id=None):
    """Test error handling and data validation."""
    logger.info("Testing error handling...")

    # Test fetching a valid block trade if we have one
    if valid_block_trade_id:
        try:
            response = client.api_client.get_block_trade(valid_block_trade_id)
            logger.info(f"✅ Successfully fetched existing block trade: {valid_block_trade_id}")
        except Exception as e:
            logger.warning(f"⚠️ Failed to fetch valid block trade {valid_block_trade_id}: {e}")
    else:
        logger.info("No valid block trade ID available for testing")

    # Test with malformed ID (this tests input validation)
    malformed_id = "invalid-block-trade-id-12345"
    try:
        response = client.api_client.get_block_trade(malformed_id)
        log_warning(f"Expected error for malformed ID '{malformed_id}' but got response")
    except Exception as e:
        log_success(f"Correctly handled malformed block trade ID: {type(e).__name__}")

    # Test listing all block trades to verify API connectivity
    try:
        response = client.api_client.list_block_trades()
        results = response.results or []
        logger.info(f"✅ Successfully listed block trades: {len(results)} found")
    except Exception as e:
        logger.warning(f"⚠️ Failed to list block trades: {e}")


def display_funding_instructions(account_summaries):
    """Display instructions for funding accounts if needed."""
    unfunded_accounts = [acc for acc in account_summaries if not acc[1]]

    if unfunded_accounts:
        logger.info("\n" + "=" * 80)
        logger.info("💰 ACCOUNT FUNDING REQUIRED")
        logger.info("=" * 80)
        logger.info("The following accounts need funding to perform actual trades:")
        logger.info("")

        for account_name, _has_funds, summary in unfunded_accounts:
            if summary:
                logger.info(f"• {account_name} ({summary.account})")
                logger.info(f"  Current Account Value: {summary.account_value} USDC")
                logger.info(f"  Required Account Value: {MINIMUM_ACCOUNT_VALUE} USDC")
                logger.info(f"  Current Free Collateral: {summary.free_collateral} USDC")
                logger.info(f"  Required Free Collateral: {MINIMUM_FREE_COLLATERAL} USDC")
                logger.info("")

        logger.info("To fund these accounts:")
        logger.info("1. Use the Paradex testnet faucet or bridge")
        logger.info("2. Transfer testnet USDC to the account addresses")
        logger.info("3. Wait for the deposits to confirm")
        logger.info("4. Re-run this test")
        logger.info("=" * 80)


def main():
    """Main integration test function."""
    log_step("🚀 Starting Block Trades Integration Test")

    # Load test accounts
    accounts = load_test_accounts()

    # Create Paradex clients
    clients = create_paradex_clients(accounts)
    log_info(f"Created {len(clients)} Paradex clients")

    # Test account creation and onboarding
    all_funded, account_summaries = test_account_creation_and_onboarding(clients)

    # Display funding instructions if needed
    if not all_funded:
        display_funding_instructions(account_summaries)

    # Continue with workflow tests
    logger.info("Proceeding with workflow demonstration using real market data...")

    # Test creating block trade
    block_trade_response = test_create_block_trade(clients[0], account_summaries)

    # Get block trade ID - either from creation response or from listing existing ones
    block_trade_id = None
    market_symbol = "ETH-USD-PERP"  # Default
    base_price = 2000.0  # Default

    if block_trade_response:
        log_success("Block trade created successfully, proceeding with offers...")

        # Get block trade ID from typed response (unpack tuple)
        response, market = block_trade_response
        block_trade_id = response.block_id
        log_info(f"Block trade ID: {block_trade_id}")

        # Get market info from the response
        trades = response.trades or {}
        market_symbol = list(trades.keys())[0] if trades else "ETH-USD-PERP"
        base_price = (
            float(trades[market_symbol].price)
            if trades and market_symbol in trades and trades[market_symbol].price
            else 2000.0
        )

        # Get tick size, step size, and min_notional from market data - no defaults allowed
        if not market:
            raise RuntimeError("No market data available from block trade creation")

        price_tick_size = market.price_tick_size
        order_size_increment = market.order_size_increment
        min_notional = market.min_notional

        if not price_tick_size or not order_size_increment or not min_notional:
            raise RuntimeError(
                f"Missing price_tick_size, order_size_increment, or min_notional in market data for {market_symbol}"
            )

        market_tick_size = float(price_tick_size)
        market_step_size = float(order_size_increment)
        market_min_notional = float(min_notional)

    else:
        log_warning("Block trade creation failed, trying to find existing block trades...")

        # Try to get an existing block trade ID from the list
        try:
            existing_trades = clients[0].api_client.list_block_trades()
            results = existing_trades.results or []
            if results:
                # Use the first available block trade
                first_trade = results[0]
                block_trade_id = first_trade.get("block_id") or first_trade.get("id")

                # Try to extract market info from existing trade
                if first_trade.get("trades"):
                    market_symbol = list(first_trade["trades"].keys())[0]
                    trade_info = first_trade["trades"][market_symbol]
                    base_price = float(trade_info.get("price", 2000.0))

                log_info(f"Using existing block trade: {block_trade_id}")
            else:
                log_warning("No existing block trades found, skipping subsequent tests")
        except Exception as e:
            log_error(f"Failed to list existing block trades: {e}")

    # Only proceed if we have a valid block trade ID
    if block_trade_id:
        # Fetch actual market data for proper tick size - STRICT API ONLY
        try:
            markets_response = clients[0].api_client.fetch_markets()
            market_data = None
            for market_dict in markets_response.get("results", []):
                if market_dict.get("symbol") == market_symbol:
                    market_data = market_dict
                    break

            if not market_data:
                raise RuntimeError(f"Market data not found for {market_symbol} in API response")

            # Extract tick size, step size, and min_notional from API - no defaults allowed
            price_tick_size = market_data.get("price_tick_size")
            order_size_increment = market_data.get("order_size_increment")
            min_notional = market_data.get("min_notional")

            if not price_tick_size or not order_size_increment or not min_notional:
                raise RuntimeError(
                    f"Missing price_tick_size, order_size_increment, or min_notional for {market_symbol}"
                )

            market_tick_size = float(price_tick_size)
            market_step_size = float(order_size_increment)
            market_min_notional = float(min_notional)
            log_info(
                f"Using market data from API: {market_symbol}, tick_size={market_tick_size},"
                f" step_size={market_step_size}, min_notional={market_min_notional}"
            )

        except Exception as e:
            log_error(f"FATAL: Failed to fetch market data: {e}")
            raise RuntimeError(f"Cannot proceed without valid market data for {market_symbol}: {e}") from e

        # Test submitting offers
        test_submit_offers(
            clients,
            block_trade_id,
            account_summaries,
            market_symbol,
            base_price,
            market_tick_size,
            market_step_size,
            market_min_notional,
        )

        # Test listing offers
        offers_response = test_list_offers(clients[0], block_trade_id)

        # Extract offer IDs if available (using 'block_id' field)
        offer_ids = []
        results = offers_response.results or [] if offers_response else []
        if results:
            offer_ids = [
                offer.get("block_id") or offer.get("id")
                for offer in results
                if offer.get("block_id") or offer.get("id")
            ]
            log_info(f"Found {len(offer_ids)} offers")

            # Log summary of offers instead of full JSON
            for i, offer in enumerate(results[:3], 1):  # Show max 3 offers
                offer_id = offer.get("block_id") or offer.get("id")
                status = offer.get("status", "Unknown")
                log_info(f"  Offer {i}: {offer_id} (Status: {status})")

        # Only execute if there are offers
        if offer_ids:
            test_execute_block_trade(clients[0], block_trade_id, account_summaries, offer_ids)

            # Example: Polling both the block trade and offers using the new polling function
            log_info("🔄 Example: Polling multiple entities (block trade + offers)")
            all_entities = [block_trade_id, *offer_ids]  # Block trade + first 2 offers
            final_statuses, completed = poll_status_for_entities(clients[0], all_entities, max_checks=10, interval=2)
            log_info(f"📋 Polling results: {len(completed)} entities completed. Final statuses: {final_statuses}")
        else:
            log_warning("No offers found, skipping execution")

        # Test cancellation operations only if we have real IDs and after execution attempts
        if offer_ids:
            log_info("Testing cancellation after execution attempts...")
            test_cancel_operations(clients[0], block_trade_id, offer_ids[:2] if len(offer_ids) >= 2 else offer_ids)
        else:
            # If no offers, still test cancelling the main block trade
            test_cancel_operations(clients[0], block_trade_id, [])

    else:
        log_warning("No valid block trade ID available, skipping subsequent tests")
        log_info("This is expected if accounts lack sufficient funds and no existing block trades are available")

    # Always test listing and error handling
    test_list_block_trades(clients[0])

    # Pass the valid block trade ID if we have one
    valid_block_trade_id = block_trade_response[0].block_id if block_trade_response else None
    test_error_handling(clients[0], valid_block_trade_id)

    log_step("🎉 Integration Test Completed Successfully!")

    if not all_funded:
        log_info("💡 Note: To test with real trades, please fund the accounts as instructed above.")


if __name__ == "__main__":
    main()
