"""Tests for odin_bots.cli.withdraw — withdraw from Odin.Fun back to wallet."""

from unittest.mock import MagicMock, patch

import pytest

M = "odin_bots.cli.withdraw"


def _make_mock_identity(principal_str="controller-principal"):
    identity = MagicMock()
    identity.sender.return_value = MagicMock(__str__=lambda s: principal_str)
    return identity


def _make_mock_auth(bot_principal="bot-principal-abc"):
    delegate = MagicMock()
    delegate.der_pubkey = b"\x30" * 44
    return {
        "delegate_identity": delegate,
        "bot_principal_text": bot_principal,
        "jwt_token": "jwt",
    }


class TestRunWithdrawSuccess:
    @patch(f"{M}.get_btc_to_usd_rate", return_value=100_000.0)
    @patch(f"{M}.transfer", return_value={"Ok": 1})
    @patch(f"{M}.get_balance")
    @patch(f"{M}.create_icrc1_canister")
    @patch(f"{M}.unwrap_canister_result", side_effect=lambda x: x)
    @patch(f"{M}.patch_delegate_sender")
    @patch(f"{M}.load_session")
    @patch(f"{M}.Canister")
    @patch(f"{M}.Agent")
    @patch(f"{M}.Client")
    @patch(f"{M}.Identity")
    def test_withdraw_specific_amount(self, MockId, MockClient, MockAgent,
                                       MockCanister, mock_load,
                                       mock_patch_del, mock_unwrap,
                                       mock_create_icrc1, mock_get_bal,
                                       mock_transfer, mock_rate,
                                       odin_project, capsys):
        mock_load.return_value = _make_mock_auth()
        MockId.from_pem.return_value = _make_mock_identity()

        # Odin canister: getBalance returns 5000 sats in msat
        mock_odin = MagicMock()
        mock_odin.getBalance.return_value = 5_000_000  # 5000 sats in msat
        mock_odin.token_withdraw.return_value = {"ok": True}
        MockCanister.side_effect = [mock_odin, mock_odin]

        # After withdrawal, bot has 4990 sats ckBTC (minus fee)
        mock_get_bal.side_effect = [4990, 0, 50000]  # bot ckbtc, bot after sweep, controller
        mock_transfer.return_value = {"Ok": 1}

        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="3000")

        output = capsys.readouterr().out
        assert "Withdrawing: 3,000 sats" in output
        assert "Withdrawal complete" in output

    @patch(f"{M}.get_btc_to_usd_rate", return_value=100_000.0)
    @patch(f"{M}.transfer", return_value={"Ok": 1})
    @patch(f"{M}.get_balance")
    @patch(f"{M}.create_icrc1_canister")
    @patch(f"{M}.unwrap_canister_result", side_effect=lambda x: x)
    @patch(f"{M}.patch_delegate_sender")
    @patch(f"{M}.load_session")
    @patch(f"{M}.Canister")
    @patch(f"{M}.Agent")
    @patch(f"{M}.Client")
    @patch(f"{M}.Identity")
    def test_withdraw_all(self, MockId, MockClient, MockAgent,
                           MockCanister, mock_load, mock_patch_del,
                           mock_unwrap, mock_create_icrc1, mock_get_bal,
                           mock_transfer, mock_rate,
                           odin_project, capsys):
        mock_load.return_value = _make_mock_auth()
        MockId.from_pem.return_value = _make_mock_identity()

        mock_odin = MagicMock()
        mock_odin.getBalance.return_value = 10_000_000  # 10000 sats
        mock_odin.token_withdraw.return_value = {"ok": True}
        MockCanister.side_effect = [mock_odin, mock_odin]

        mock_get_bal.side_effect = [9990, 0, 60000]
        mock_transfer.return_value = {"Ok": 1}

        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="all")

        output = capsys.readouterr().out
        assert "Withdrawing ALL" in output
        assert "Withdrawal complete" in output


class TestRunWithdrawErrors:
    def test_no_wallet(self, odin_project_no_wallet, capsys):
        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="1000")
        output = capsys.readouterr().out
        assert "No odin-bots wallet found" in output

    @patch(f"{M}.get_btc_to_usd_rate", return_value=100_000.0)
    @patch(f"{M}.unwrap_canister_result", side_effect=lambda x: x)
    @patch(f"{M}.patch_delegate_sender")
    @patch(f"{M}.load_session")
    @patch(f"{M}.Canister")
    @patch(f"{M}.Agent")
    @patch(f"{M}.Client")
    def test_insufficient_balance(self, MockClient, MockAgent, MockCanister,
                                   mock_load, mock_patch_del, mock_unwrap,
                                   mock_rate, odin_project, capsys):
        mock_load.return_value = _make_mock_auth()
        mock_odin = MagicMock()
        mock_odin.getBalance.return_value = 500_000  # 500 sats
        MockCanister.side_effect = [mock_odin, mock_odin]

        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="10000")

        output = capsys.readouterr().out
        assert "Insufficient balance" in output

    @patch(f"{M}.get_btc_to_usd_rate", return_value=100_000.0)
    @patch(f"{M}.unwrap_canister_result", side_effect=lambda x: x)
    @patch(f"{M}.patch_delegate_sender")
    @patch(f"{M}.load_session")
    @patch(f"{M}.Canister")
    @patch(f"{M}.Agent")
    @patch(f"{M}.Client")
    def test_zero_balance(self, MockClient, MockAgent, MockCanister,
                           mock_load, mock_patch_del, mock_unwrap,
                           mock_rate, odin_project, capsys):
        mock_load.return_value = _make_mock_auth()
        mock_odin = MagicMock()
        mock_odin.getBalance.return_value = 0
        MockCanister.side_effect = [mock_odin, mock_odin]

        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="all")

        output = capsys.readouterr().out
        assert "No funds to withdraw" in output

    @patch(f"{M}.get_btc_to_usd_rate", return_value=100_000.0)
    @patch(f"{M}.unwrap_canister_result", side_effect=lambda x: x)
    @patch(f"{M}.patch_delegate_sender")
    @patch(f"{M}.load_session")
    @patch(f"{M}.Canister")
    @patch(f"{M}.Agent")
    @patch(f"{M}.Client")
    def test_withdraw_canister_error(self, MockClient, MockAgent, MockCanister,
                                      mock_load, mock_patch_del, mock_unwrap,
                                      mock_rate, odin_project, capsys):
        mock_load.return_value = _make_mock_auth()
        mock_odin = MagicMock()
        mock_odin.getBalance.return_value = 5_000_000
        mock_odin.token_withdraw.return_value = {"err": "withdrawal error"}
        MockCanister.side_effect = [mock_odin, mock_odin]

        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="1000")

        output = capsys.readouterr().out
        assert "FAILED" in output

    @patch(f"{M}.get_btc_to_usd_rate", return_value=100_000.0)
    @patch(f"{M}.get_balance", return_value=5)  # Below fee
    @patch(f"{M}.create_icrc1_canister")
    @patch(f"{M}.unwrap_canister_result", side_effect=lambda x: x)
    @patch(f"{M}.patch_delegate_sender")
    @patch(f"{M}.load_session")
    @patch(f"{M}.Canister")
    @patch(f"{M}.Agent")
    @patch(f"{M}.Client")
    def test_sweep_skipped_when_balance_too_low(self, MockClient, MockAgent,
                                                  MockCanister, mock_load,
                                                  mock_patch_del, mock_unwrap,
                                                  mock_create_icrc1,
                                                  mock_get_bal, mock_rate,
                                                  odin_project, capsys):
        mock_load.return_value = _make_mock_auth()
        mock_odin = MagicMock()
        mock_odin.getBalance.return_value = 5_000_000
        mock_odin.token_withdraw.return_value = {"ok": True}
        MockCanister.side_effect = [mock_odin, mock_odin]

        from odin_bots.cli.withdraw import run_withdraw
        run_withdraw(bot_name="bot-1", amount="1000")

        output = capsys.readouterr().out
        assert "too low to transfer" in output
