"""Tests for TradingSetup helper methods.

Test Categories:
1. find_strategy - Find strategy by name
2. find_account - Find account by name
3. find_portfolio - Find portfolio by name
4. find_available_slot - Find first unbound slot
5. find_binding - Find binding by account_id
6. bind_portfolio - account_source validation and BindingError
"""

from datetime import datetime
from unittest.mock import MagicMock, patch

import pytest
from tradepose_client.exceptions import BindingError
from tradepose_client.setup.trading_setup import (
    AccountInfo,
    BindingInfo,
    BlueprintInfo,
    PortfolioInfo,
    SelectionInfo,
    SlotInfo,
    StrategyInfo,
    TradingSetup,
)

# =============================================================================
# FIXTURES
# =============================================================================


@pytest.fixture
def mock_strategies():
    """Sample strategy list."""
    return [
        StrategyInfo(
            id="strat-1",
            name="SuperTrend_BTCUSDT",
            base_instrument="BTCUSDT",
            base_freq="1h",
            note="Test strategy 1",
            base_blueprint=BlueprintInfo(id="bp-1", name="base_bp"),
            advanced_blueprints=[],
        ),
        StrategyInfo(
            id="strat-2",
            name="SMA_Crossover_ETHUSDT",
            base_instrument="ETHUSDT",
            base_freq="4h",
            note="Test strategy 2",
            base_blueprint=BlueprintInfo(id="bp-2", name="sma_bp"),
            advanced_blueprints=[BlueprintInfo(id="bp-3", name="adv_bp")],
        ),
    ]


@pytest.fixture
def mock_accounts():
    """Sample account list."""
    return [
        AccountInfo(
            id="acc-1",
            name="binance_main",
            broker_type="binance",
            available_markets=["spot", "futures"],
            status="active",
            is_archived=False,
            account_source="BINANCE",
        ),
        AccountInfo(
            id="acc-2",
            name="ftmo_account",
            broker_type="mt5",
            available_markets=["forex"],
            status="active",
            is_archived=False,
            account_source="FTMO",
        ),
    ]


@pytest.fixture
def mock_portfolios():
    """Sample portfolio list."""
    return [
        PortfolioInfo(
            id="port-1",
            name="binance_portfolio",
            capital="10000",
            currency="USDT",
            account_source="BINANCE",
            selections=[
                SelectionInfo(
                    strategy_id="strat-1",
                    blueprint_id="bp-1",
                    strategy_name="SuperTrend_BTCUSDT",
                    blueprint_name="base_bp",
                )
            ],
        ),
        PortfolioInfo(
            id="port-2",
            name="ftmo_portfolio",
            capital="100000",
            currency="USD",
            account_source="FTMO",
            selections=[],
        ),
    ]


@pytest.fixture
def mock_slots():
    """Sample slot list."""
    return [
        SlotInfo(
            id="slot-1",
            node_seq=0,
            slot_idx=0,
            account_id="acc-1",
            bound_at=datetime(2025, 1, 1, 0, 0, 0),
        ),
        SlotInfo(
            id="slot-2",
            node_seq=0,
            slot_idx=1,
            account_id=None,  # Available
            bound_at=None,
        ),
        SlotInfo(
            id="slot-3",
            node_seq=1,
            slot_idx=0,
            account_id=None,  # Available
            bound_at=None,
        ),
    ]


@pytest.fixture
def mock_bindings():
    """Sample binding list."""
    return [
        BindingInfo(
            id="bind-1",
            account_id="acc-1",
            portfolio_id="port-1",
            execution_mode="signal_priority",
            is_active=True,
        ),
    ]


@pytest.fixture
def trading_setup():
    """Create TradingSetup instance for testing."""
    return TradingSetup(api_key="test_key", server_url="https://test.api.com")


# =============================================================================
# TEST: find_strategy
# =============================================================================


class TestFindStrategy:
    """Tests for find_strategy method."""

    def test_find_existing_strategy(self, trading_setup, mock_strategies):
        """Find strategy that exists."""
        with patch.object(trading_setup, "list_strategies", return_value=mock_strategies):
            result = trading_setup.find_strategy("SuperTrend_BTCUSDT")

        assert result is not None
        assert result.id == "strat-1"
        assert result.name == "SuperTrend_BTCUSDT"

    def test_find_nonexistent_strategy(self, trading_setup, mock_strategies):
        """Return None for nonexistent strategy."""
        with patch.object(trading_setup, "list_strategies", return_value=mock_strategies):
            result = trading_setup.find_strategy("NonExistent_Strategy")

        assert result is None

    def test_find_strategy_empty_list(self, trading_setup):
        """Return None when no strategies exist."""
        with patch.object(trading_setup, "list_strategies", return_value=[]):
            result = trading_setup.find_strategy("AnyStrategy")

        assert result is None

    def test_find_strategy_by_id(self, trading_setup, mock_strategies):
        """Find strategy by UUID."""
        with patch.object(trading_setup, "list_strategies", return_value=mock_strategies):
            result = trading_setup.find_strategy("strat-2")

        assert result is not None
        assert result.id == "strat-2"
        assert result.name == "SMA_Crossover_ETHUSDT"

    def test_find_strategy_case_sensitive(self, trading_setup, mock_strategies):
        """Strategy name matching is case-sensitive."""
        with patch.object(trading_setup, "list_strategies", return_value=mock_strategies):
            result = trading_setup.find_strategy("supertrend_btcusdt")

        assert result is None


# =============================================================================
# TEST: find_account
# =============================================================================


class TestFindAccount:
    """Tests for find_account method."""

    def test_find_existing_account(self, trading_setup, mock_accounts):
        """Find account that exists."""
        with patch.object(trading_setup, "list_accounts", return_value=mock_accounts):
            result = trading_setup.find_account("binance_main")

        assert result is not None
        assert result.id == "acc-1"
        assert result.name == "binance_main"
        assert result.account_source == "BINANCE"

    def test_find_nonexistent_account(self, trading_setup, mock_accounts):
        """Return None for nonexistent account."""
        with patch.object(trading_setup, "list_accounts", return_value=mock_accounts):
            result = trading_setup.find_account("nonexistent_account")

        assert result is None

    def test_find_account_empty_list(self, trading_setup):
        """Return None when no accounts exist."""
        with patch.object(trading_setup, "list_accounts", return_value=[]):
            result = trading_setup.find_account("any_account")

        assert result is None

    def test_find_account_by_id(self, trading_setup, mock_accounts):
        """Find account by UUID."""
        with patch.object(trading_setup, "list_accounts", return_value=mock_accounts):
            result = trading_setup.find_account("acc-2")

        assert result is not None
        assert result.id == "acc-2"
        assert result.name == "ftmo_account"


# =============================================================================
# TEST: find_portfolio
# =============================================================================


class TestFindPortfolio:
    """Tests for find_portfolio method."""

    def test_find_existing_portfolio(self, trading_setup, mock_portfolios):
        """Find portfolio that exists."""
        with patch.object(trading_setup, "list_portfolios", return_value=mock_portfolios):
            result = trading_setup.find_portfolio("binance_portfolio")

        assert result is not None
        assert result.name == "binance_portfolio"
        assert result.account_source == "BINANCE"

    def test_find_nonexistent_portfolio(self, trading_setup, mock_portfolios):
        """Return None for nonexistent portfolio."""
        with patch.object(trading_setup, "list_portfolios", return_value=mock_portfolios):
            result = trading_setup.find_portfolio("nonexistent_portfolio")

        assert result is None

    def test_find_portfolio_empty_list(self, trading_setup):
        """Return None when no portfolios exist."""
        with patch.object(trading_setup, "list_portfolios", return_value=[]):
            result = trading_setup.find_portfolio("any_portfolio")

        assert result is None

    def test_find_portfolio_by_id(self, trading_setup, mock_portfolios):
        """Find portfolio by UUID."""
        with patch.object(trading_setup, "list_portfolios", return_value=mock_portfolios):
            result = trading_setup.find_portfolio("port-2")

        assert result is not None
        assert result.id == "port-2"
        assert result.name == "ftmo_portfolio"


# =============================================================================
# TEST: find_available_slot
# =============================================================================


class TestFindAvailableSlot:
    """Tests for find_available_slot method."""

    def test_find_first_available_slot(self, trading_setup, mock_slots):
        """Find first slot with account_id=None."""
        with patch.object(trading_setup, "list_slots", return_value=mock_slots):
            result = trading_setup.find_available_slot()

        assert result is not None
        assert result.id == "slot-2"
        assert result.account_id is None

    def test_no_available_slots(self, trading_setup):
        """Return None when all slots are bound."""
        all_bound = [
            SlotInfo(
                id="slot-1",
                node_seq=0,
                slot_idx=0,
                account_id="acc-1",
                bound_at=datetime.now(),
            ),
        ]
        with patch.object(trading_setup, "list_slots", return_value=all_bound):
            result = trading_setup.find_available_slot()

        assert result is None

    def test_find_available_slot_empty_list(self, trading_setup):
        """Return None when no slots exist."""
        with patch.object(trading_setup, "list_slots", return_value=[]):
            result = trading_setup.find_available_slot()

        assert result is None


# =============================================================================
# TEST: find_binding
# =============================================================================


class TestFindBinding:
    """Tests for find_binding method."""

    def test_find_binding_by_account_id(self, trading_setup, mock_bindings):
        """Find binding by account_id."""
        with patch.object(trading_setup, "list_bindings", return_value=mock_bindings):
            result = trading_setup.find_binding(account_id="acc-1")

        assert result is not None
        assert result.id == "bind-1"
        assert result.account_id == "acc-1"

    def test_find_binding_nonexistent(self, trading_setup, mock_bindings):
        """Return None for nonexistent binding."""
        with patch.object(trading_setup, "list_bindings", return_value=mock_bindings):
            result = trading_setup.find_binding(account_id="nonexistent-acc")

        assert result is None

    def test_find_binding_empty_list(self, trading_setup):
        """Return None when no bindings exist."""
        with patch.object(trading_setup, "list_bindings", return_value=[]):
            result = trading_setup.find_binding(account_id="any-acc")

        assert result is None


# =============================================================================
# TEST: bind_portfolio - account_source validation
# =============================================================================


class TestBindPortfolioValidation:
    """Tests for bind_portfolio account_source validation."""

    def test_bind_portfolio_account_not_found(self, trading_setup):
        """Raise BindingError when account not found."""
        with patch.object(trading_setup, "list_accounts", return_value=[]):
            with pytest.raises(BindingError) as exc_info:
                trading_setup.bind_portfolio("nonexistent-acc", "port-1")

        assert "not found" in str(exc_info.value)
        assert "list_accounts()" in str(exc_info.value)

    def test_bind_portfolio_portfolio_not_found(self, trading_setup, mock_accounts):
        """Raise BindingError when portfolio not found."""
        with (
            patch.object(trading_setup, "list_accounts", return_value=mock_accounts),
            patch.object(trading_setup, "list_portfolios", return_value=[]),
        ):
            with pytest.raises(BindingError) as exc_info:
                trading_setup.bind_portfolio("acc-1", "nonexistent-port")

        assert "not found" in str(exc_info.value)
        assert "list_portfolios()" in str(exc_info.value)

    def test_bind_portfolio_account_source_mismatch(
        self, trading_setup, mock_accounts, mock_portfolios
    ):
        """Raise BindingError when account_source doesn't match."""
        # acc-1 has account_source="BINANCE", ftmo_portfolio (port-2) has account_source="FTMO"
        with (
            patch.object(trading_setup, "list_accounts", return_value=mock_accounts),
            patch.object(trading_setup, "list_portfolios", return_value=mock_portfolios),
        ):
            with pytest.raises(BindingError) as exc_info:
                trading_setup.bind_portfolio("acc-1", "port-2")  # FTMO portfolio

        error = exc_info.value
        assert "BINANCE" in str(error)
        assert "FTMO" in str(error)
        assert error.account_source == "BINANCE"
        assert error.portfolio_source == "FTMO"
        assert error.suggestion is not None

    def test_bind_portfolio_success(self, trading_setup, mock_accounts, mock_portfolios):
        """Successful binding when account_source matches."""
        # acc-1 has account_source="BINANCE", binance_portfolio (port-1) has account_source="BINANCE"
        mock_binding = BindingInfo(
            id="new-bind",
            account_id="acc-1",
            portfolio_id="port-1",
            execution_mode="signal_priority",
            is_active=True,
        )

        with (
            patch.object(trading_setup, "list_accounts", return_value=mock_accounts),
            patch.object(trading_setup, "list_portfolios", return_value=mock_portfolios),
            patch.object(trading_setup, "_run_async", return_value=mock_binding),
        ):
            result = trading_setup.bind_portfolio("acc-1", "port-1")  # BINANCE portfolio

        assert result is not None
        assert result.id == "new-bind"

    def test_bind_portfolio_skip_validation(self, trading_setup):
        """Skip validation avoids redundant API calls."""
        mock_binding = BindingInfo(
            id="new-bind",
            account_id="acc-1",
            portfolio_id="port-1",
            execution_mode="signal_priority",
            is_active=True,
        )

        # Mock list methods to track if they're called
        list_accounts_mock = MagicMock()
        list_portfolios_mock = MagicMock()

        with (
            patch.object(trading_setup, "list_accounts", list_accounts_mock),
            patch.object(trading_setup, "list_portfolios", list_portfolios_mock),
            patch.object(trading_setup, "_run_async", return_value=mock_binding),
        ):
            result = trading_setup.bind_portfolio("acc-1", "port-1", skip_validation=True)

        # Verify list methods were NOT called
        list_accounts_mock.assert_not_called()
        list_portfolios_mock.assert_not_called()
        assert result is not None
        assert result.id == "new-bind"


# =============================================================================
# TEST: BindingError
# =============================================================================


class TestBindingError:
    """Tests for BindingError exception."""

    def test_binding_error_basic(self):
        """Basic BindingError creation."""
        error = BindingError("Test error message")
        assert "Test error message" in str(error)

    def test_binding_error_with_suggestion(self):
        """BindingError includes suggestion in message."""
        error = BindingError(
            "Account source mismatch",
            suggestion="Use an account with matching source.",
        )
        assert "Account source mismatch" in str(error)
        assert "Use an account with matching source" in str(error)

    def test_binding_error_attributes(self):
        """BindingError stores all attributes."""
        error = BindingError(
            "Mismatch error",
            account_name="test_account",
            portfolio_name="test_portfolio",
            account_source="BINANCE",
            portfolio_source="FTMO",
            suggestion="Fix the mismatch",
        )
        assert error.account_name == "test_account"
        assert error.portfolio_name == "test_portfolio"
        assert error.account_source == "BINANCE"
        assert error.portfolio_source == "FTMO"
        assert error.suggestion == "Fix the mismatch"
