"""handler.amend_trade / cancel_active_card 회귀 테스트.

활성 카드 머지 → 가드 재실행 → PIN 재발급 흐름 검증.
0 입력 시 cancel 위임, ambiguous 입력 거부, account/symbol/side 변경 불가.
"""

from __future__ import annotations

import unittest
from datetime import datetime, timedelta, timezone
from unittest import mock

from orders import handler, ledger, pin

KST = timezone(timedelta(hours=9))


def _md(orderbook=None):
    return {
        'now': datetime(2026, 5, 8, 11, 30, tzinfo=KST),
        'is_holiday': False,
        'nxt_eligible': True,
        'current_price': 75050,
        'prev_close': 75000,
        'upper_limit': 100000,
        'lower_limit': 50000,
        'halt': False,
        'vi': False,
        'orderbook': orderbook if orderbook is not None else {
            'asks': [{'price': 75100, 'qty': 200}, {'price': 75200, 'qty': 150}],
            'bids': [{'price': 75000, 'qty': 180}, {'price': 74900, 'qty': 220}],
        },
        'broker_executions': [],
        'broker_query_error': None,
        'balance_d2': 5_000_000,
    }


def _active_card(payload=None, expired=False, consumed=False):
    p = mock.MagicMock(spec=pin.PendingCard)
    p.card_id = 'A7K3'
    p.pin = '1234'
    p.account_label = '일반'
    p.payload = payload or {
        'account': '일반', 'side': 'BUY', 'symbol': '005930', 'symbol_name': '삼성전자',
        'qty': 14, 'price': None, 'order_type': 'MARKET', 'routing_suffix': '_AL',
    }
    p.consumed = consumed
    p.is_expired = mock.MagicMock(return_value=expired)
    return p


class HandlerAmendTests(unittest.TestCase):
    def setUp(self):
        self.collect = mock.patch.object(handler.datasource, 'collect_market_data',
                                          return_value=_md()).start()
        mock.patch.object(handler.sidecar, 'guard_or_raise', return_value=None).start()
        mock.patch.object(handler, '_sweep_expired_and_notify', return_value=None).start()
        mock.patch.object(handler.ledger, 'append', return_value=None).start()
        mock.patch.object(ledger, 'last_terminal_event', return_value=None).start()
        mock.patch.object(ledger, 'last_event_for_symbol', return_value=None).start()
        # 활성 카드 mock — peek 으로 반환
        self.active = _active_card()
        mock.patch.object(handler._pin_store, 'peek', return_value=self.active).start()
        # amend 호출 시 새 PendingCard 반환
        self.amended_pending = mock.MagicMock()
        self.amended_pending.card_id = 'A7K3'
        self.amended_pending.pin = '5678'
        self.amended_pending.expiry_seconds = 120
        mock.patch.object(handler._pin_store, 'amend', return_value=self.amended_pending).start()
        # cancel mock — cancel 위임 검증용
        self.cancel_card = _active_card()
        mock.patch.object(handler._pin_store, 'cancel', return_value=self.cancel_card).start()
        self.addCleanup(mock.patch.stopall)

    def test_amend_qty_only_succeeds(self):
        res = handler.amend_trade(qty=20)
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        self.assertEqual(new_payload['qty'], 20)
        self.assertEqual(new_payload['order_type'], 'MARKET')  # 기존 유지
        self.assertEqual(new_payload['symbol'], '005930')      # 기존 유지

    def test_amend_price_only_changes_to_limit_keeps_existing_type(self):
        # 기존 MARKET 유지, price 만 변경 — order_type 안 줬으면 그대로 MARKET
        res = handler.amend_trade(price=75500)
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        # MARKET 일 땐 final_price 가 None (estimate 만 사용)
        self.assertEqual(new_payload['order_type'], 'MARKET')

    def test_amend_order_type_to_limit_with_price(self):
        res = handler.amend_trade(order_type='LIMIT', price=75500)
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        self.assertEqual(new_payload['order_type'], 'LIMIT')
        self.assertEqual(new_payload['price'], 75500)

    def test_amend_budget_changes_qty_and_forces_market(self):
        res = handler.amend_trade(budget=2_000_000)
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        # 2,000,000 / 75,100 = 26.6 → floor 26 + bump → 27주 (75,100 ≤ 30만원)
        self.assertEqual(new_payload['qty'], 27)
        self.assertEqual(new_payload['order_type'], 'MARKET')

    def test_amend_zero_qty_delegates_to_cancel(self):
        res = handler.amend_trade(qty=0)
        self.assertTrue(res['ok'])
        # cancel 호출됨, amend 호출 안 됨
        handler._pin_store.cancel.assert_called_once()
        handler._pin_store.amend.assert_not_called()

    def test_amend_zero_price_delegates_to_cancel(self):
        res = handler.amend_trade(price=0)
        self.assertTrue(res['ok'])
        handler._pin_store.cancel.assert_called_once()
        handler._pin_store.amend.assert_not_called()

    def test_amend_zero_budget_delegates_to_cancel(self):
        res = handler.amend_trade(budget=0)
        self.assertTrue(res['ok'])
        handler._pin_store.cancel.assert_called_once()

    def test_amend_zero_with_nonzero_rejected(self):
        # qty=0 + price=75000 동시 → AMBIGUOUS_AMEND
        res = handler.amend_trade(qty=0, price=75000)
        self.assertFalse(res['ok'])
        self.assertIn('AMBIGUOUS_AMEND', res['message'])
        handler._pin_store.cancel.assert_not_called()

    def test_amend_zero_with_order_type_rejected(self):
        res = handler.amend_trade(qty=0, order_type='LIMIT')
        self.assertFalse(res['ok'])
        self.assertIn('AMBIGUOUS_AMEND', res['message'])

    def test_amend_no_active_card_rejected(self):
        handler._pin_store.peek.return_value = None
        res = handler.amend_trade(qty=20)
        self.assertFalse(res['ok'])
        self.assertIn('NO_ACTIVE_CARD', res['message'])

    def test_amend_expired_card_rejected(self):
        handler._pin_store.peek.return_value = _active_card(expired=True)
        res = handler.amend_trade(qty=20)
        self.assertFalse(res['ok'])
        self.assertIn('NO_ACTIVE_CARD', res['message'])

    def test_amend_consumed_card_rejected(self):
        handler._pin_store.peek.return_value = _active_card(consumed=True)
        res = handler.amend_trade(qty=20)
        self.assertFalse(res['ok'])
        self.assertIn('NO_ACTIVE_CARD', res['message'])

    def test_amend_qty_and_budget_both_rejected(self):
        res = handler.amend_trade(qty=20, budget=1_000_000)
        self.assertFalse(res['ok'])
        self.assertIn('AMBIGUOUS_INPUT', res['message'])

    def test_amend_blocked_by_balance_guard(self):
        # 잔고를 50만원으로 낮추고 100주로 늘리면 guards.validate_request 의 INSUFFICIENT_BALANCE 에서 거부
        md = _md()
        md['balance_d2'] = 500_000
        self.collect.return_value = md
        res = handler.amend_trade(qty=100)
        self.assertFalse(res['ok'])
        self.assertIn('INSUFFICIENT_BALANCE', res['message'])
        handler._pin_store.amend.assert_not_called()

    def test_amend_invalid_order_type_rejected(self):
        res = handler.amend_trade(order_type='WEIRD')
        self.assertFalse(res['ok'])
        self.assertIn('INVALID_ORDER_TYPE', res['message'])

    def test_amend_card_carries_amended_marker(self):
        res = handler.amend_trade(qty=20)
        self.assertTrue(res['ok'])
        self.assertIn('수정됨', res['card_message'])

    def test_amend_returns_new_pin(self):
        res = handler.amend_trade(qty=20)
        self.assertTrue(res['ok'])
        # PIN 재발급 — amend mock 이 5678 반환
        self.assertEqual(res['pin_message'], '5678')

    def test_amend_account_only(self):
        res = handler.amend_trade(account='ISA')
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload, kwargs = (handler._pin_store.amend.call_args[0],
                                handler._pin_store.amend.call_args[1])
        self.assertEqual(new_payload[0]['account'], 'ISA')
        self.assertEqual(new_payload[0]['symbol'], '005930')   # 종목 유지
        self.assertEqual(kwargs.get('account_label'), 'ISA')   # PinStore.amend 에 새 account 전달

    def test_amend_account_to_spouse_passes_new_account_label(self):
        res = handler.amend_trade(account='가희_ISA')
        self.assertTrue(res['ok'], msg=res.get('message'))
        kwargs = handler._pin_store.amend.call_args[1]
        self.assertEqual(kwargs.get('account_label'), '가희_ISA')

    def test_amend_invalid_account_rejected(self):
        res = handler.amend_trade(account='UNKNOWN')
        self.assertFalse(res['ok'])
        self.assertIn('INVALID_ACCOUNT', res['message'])
        handler._pin_store.amend.assert_not_called()

    def test_amend_symbol_with_name(self):
        res = handler.amend_trade(symbol='035720', symbol_name='카카오')
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        self.assertEqual(new_payload['symbol'], '035720')
        self.assertEqual(new_payload['symbol_name'], '카카오')
        self.assertEqual(new_payload['account'], '일반')  # 계좌 유지

    def test_amend_symbol_without_name_rejected(self):
        res = handler.amend_trade(symbol='035720')
        self.assertFalse(res['ok'])
        self.assertIn('MISSING_SYMBOL_NAME', res['message'])
        handler._pin_store.amend.assert_not_called()

    def test_amend_invalid_symbol_format_rejected(self):
        # 5자리 — 6자리 강제
        res = handler.amend_trade(symbol='12345', symbol_name='임시')
        self.assertFalse(res['ok'])
        self.assertIn('INVALID_SYMBOL', res['message'])

    def test_amend_invalid_symbol_non_digit_rejected(self):
        res = handler.amend_trade(symbol='ABC123', symbol_name='임시')
        self.assertFalse(res['ok'])
        self.assertIn('INVALID_SYMBOL', res['message'])

    def test_amend_account_and_symbol_together(self):
        res = handler.amend_trade(account='ISA', symbol='035720', symbol_name='카카오', qty=10)
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        self.assertEqual(new_payload['account'], 'ISA')
        self.assertEqual(new_payload['symbol'], '035720')
        self.assertEqual(new_payload['symbol_name'], '카카오')
        self.assertEqual(new_payload['qty'], 10)

    def test_amend_zero_with_account_change_rejected(self):
        # 0 + account 변경 동시 → AMBIGUOUS_AMEND
        res = handler.amend_trade(qty=0, account='ISA')
        self.assertFalse(res['ok'])
        self.assertIn('AMBIGUOUS_AMEND', res['message'])

    def test_amend_zero_with_symbol_change_rejected(self):
        res = handler.amend_trade(qty=0, symbol='035720', symbol_name='카카오')
        self.assertFalse(res['ok'])
        self.assertIn('AMBIGUOUS_AMEND', res['message'])

    def test_amend_symbol_unchanged_no_name_required(self):
        # 같은 종목코드 다시 입력은 변경 아님 — symbol_name 미입력 OK
        res = handler.amend_trade(symbol='005930')
        self.assertTrue(res['ok'], msg=res.get('message'))

    def test_amend_orphan_symbol_name_rejected(self):
        # symbol_name 만 단독 변경 → 카드 이름 vs 발주 코드 불일치 위험 → 거부
        res = handler.amend_trade(symbol_name='카카오')
        self.assertFalse(res['ok'])
        self.assertIn('ORPHAN_SYMBOL_NAME', res['message'])
        handler._pin_store.amend.assert_not_called()

    def test_amend_same_symbol_name_no_change_passes(self):
        # 같은 이름 다시 입력은 변경 아님 → OK
        res = handler.amend_trade(symbol_name='삼성전자')
        self.assertTrue(res['ok'], msg=res.get('message'))

    def test_amend_market_to_limit_without_price_rejected(self):
        # 활성 카드가 MARKET 인 상태에서 LIMIT 으로만 전환 → price 없으면 거부
        # (기본 active fixture 가 order_type=MARKET, price=None)
        res = handler.amend_trade(order_type='LIMIT')
        self.assertFalse(res['ok'])
        self.assertIn('MISSING_LIMIT_PRICE', res['message'])
        handler._pin_store.amend.assert_not_called()

    def test_amend_market_to_limit_with_price_passes(self):
        res = handler.amend_trade(order_type='LIMIT', price=75500)
        self.assertTrue(res['ok'], msg=res.get('message'))
        new_payload = handler._pin_store.amend.call_args[0][0]
        self.assertEqual(new_payload['order_type'], 'LIMIT')
        self.assertEqual(new_payload['price'], 75500)


class SubmitPinZeroCancelTests(unittest.TestCase):
    """PIN echo 자리에 '0' 입력 → cancel 위임 검증."""

    def setUp(self):
        mock.patch.object(handler.sidecar, 'guard_or_raise', return_value=None).start()
        mock.patch.object(handler, '_sweep_expired_and_notify', return_value=None).start()
        mock.patch.object(handler.ledger, 'append', return_value=None).start()
        cancel_card = _active_card()
        mock.patch.object(handler._pin_store, 'cancel', return_value=cancel_card).start()
        self.verify = mock.patch.object(handler._pin_store, 'verify').start()
        self.addCleanup(mock.patch.stopall)

    def test_pin_zero_delegates_to_cancel(self):
        res = handler.submit_with_pin('0', dry_run=False)
        self.assertTrue(res['ok'])
        # verify 는 호출 안 됨 — 0 분기에서 즉시 cancel
        self.verify.assert_not_called()
        handler._pin_store.cancel.assert_called_once()
        self.assertIn('취소', res['message'])

    def test_pin_zero_with_whitespace_delegates_to_cancel(self):
        # 양옆 공백 strip
        res = handler.submit_with_pin('  0  ', dry_run=False)
        self.assertTrue(res['ok'])
        self.verify.assert_not_called()

    def test_pin_zero_with_no_active_card_returns_no_card(self):
        handler._pin_store.cancel.return_value = None
        res = handler.submit_with_pin('0', dry_run=False)
        self.assertFalse(res['ok'])
        self.assertIn('활성 카드 없음', res['message'])

    def test_pin_nonzero_proceeds_normal_flow(self):
        # "1234" 같은 정상 PIN 은 verify 경로 그대로
        self.verify.return_value = (False, '활성 카드 없음', None)
        res = handler.submit_with_pin('1234', dry_run=False)
        self.assertFalse(res['ok'])
        self.verify.assert_called_once()
        # cancel 은 호출 안 됨
        handler._pin_store.cancel.assert_not_called()


if __name__ == '__main__':
    unittest.main(verbosity=2)
