"""fill_watcher 회귀 테스트.

- _FillWatcher._poll_once: 부분/완전체결, 사후거절, 타임아웃, 중복 방지, 계좌 묶음 fetch.
- 큐 파일 IO: append, read, persist, 빈 큐 처리.
- watch(): 큐 append + 데몬 ensure (subprocess.Popen mock).
- is_daemon_alive: PID 파일 stale 검출.
"""

from __future__ import annotations

import os
import time
import unittest
from unittest import mock

from orders import fill_watcher
from orders.fill_watcher import Tracked


def _row(ord_no='100001', cntr_qty=0, cntr_uv=0, mdfy_cncl=''):
    return {
        'ord_no': ord_no,
        'ord_tm': '10:00:00',
        'code': '005930',
        'name': '삼성전자',
        'side': 'BUY',
        'order_qty': 10,
        'cntr_qty': cntr_qty,
        'cntr_uv': cntr_uv,
        'ord_uv': 75000,
        'order_type': '지정가',
        'exchange': 'KRX',
        'comm_src': 'REST API',
        'mdfy_cncl': mdfy_cncl,
    }


class FillWatcherPollTests(unittest.TestCase):
    """_FillWatcher._poll_once 직접 호출로 격리 (시간 의존 X)."""

    def setUp(self):
        fill_watcher._reset_for_test()
        self.sent = []
        self.fetch = mock.MagicMock(return_value=[])
        self.fetch_open = mock.MagicMock(return_value=[])
        fill_watcher.configure(send_func=lambda msg: self.sent.append(msg) or True,
                                 fetch_executions=self.fetch,
                                 fetch_open_orders=self.fetch_open)

    def _track(self, ord_no='100001', order_qty=10):
        w = fill_watcher._watcher
        with w._lock:
            w._tracked[ord_no] = Tracked(
                ord_no=ord_no, account='일반', side='BUY', symbol='005930',
                symbol_name='삼성전자', order_qty=order_qty, price=75000,
                order_type='LIMIT', card_id='A7K3', started_at=time.time(),
            )

    def test_no_row_no_alert(self):
        self._track()
        self.fetch.return_value = []
        fill_watcher._watcher._poll_once()
        self.assertEqual(self.sent, [])
        self.assertIn('100001', fill_watcher._peek_for_test())

    def test_full_fill_alerts_and_removes(self):
        self._track()
        self.fetch.return_value = [_row(cntr_qty=10, cntr_uv=75050)]
        fill_watcher._watcher._poll_once()
        self.assertEqual(len(self.sent), 1)
        self.assertIn('체결', self.sent[0])
        self.assertNotIn('100001', fill_watcher._peek_for_test())

    def test_partial_fill_alerts_and_keeps(self):
        self._track()
        self.fetch.return_value = [_row(cntr_qty=3, cntr_uv=75050)]
        fill_watcher._watcher._poll_once()
        self.assertIn('부분체결', self.sent[0])
        self.assertIn('100001', fill_watcher._peek_for_test())
        self.assertEqual(fill_watcher._peek_for_test()['100001'].last_cntr_qty, 3)

    def test_partial_then_full(self):
        self._track()
        self.fetch.return_value = [_row(cntr_qty=3, cntr_uv=75050)]
        fill_watcher._watcher._poll_once()
        self.fetch.return_value = [_row(cntr_qty=10, cntr_uv=75080)]
        fill_watcher._watcher._poll_once()
        self.assertEqual(len(self.sent), 2)
        self.assertIn('부분체결', self.sent[0])
        self.assertIn('체결', self.sent[1])
        self.assertNotIn('100001', fill_watcher._peek_for_test())

    def test_post_reject(self):
        self._track()
        self.fetch.return_value = [_row(cntr_qty=0, mdfy_cncl='취소')]
        fill_watcher._watcher._poll_once()
        self.assertIn('사후거절', self.sent[0])
        self.assertNotIn('100001', fill_watcher._peek_for_test())

    def test_timeout(self):
        self._track()
        with fill_watcher._watcher._lock:
            fill_watcher._watcher._tracked['100001'].started_at = time.time() - 1801
        self.fetch.return_value = []
        fill_watcher._watcher._poll_once()
        self.assertIn('미체결', self.sent[0])
        self.assertNotIn('100001', fill_watcher._peek_for_test())

    def test_no_duplicate_alert_on_same_state(self):
        self._track()
        self.fetch.return_value = [_row(cntr_qty=3, cntr_uv=75050)]
        fill_watcher._watcher._poll_once()
        fill_watcher._watcher._poll_once()
        fill_watcher._watcher._poll_once()
        self.assertEqual(len(self.sent), 1)

    def test_one_fetch_per_account(self):
        self._track(ord_no='100001')
        self._track(ord_no='100002')
        self.fetch.return_value = [
            _row(ord_no='100001', cntr_qty=10, cntr_uv=75100),
            _row(ord_no='100002', cntr_qty=5, cntr_uv=75100),
        ]
        fill_watcher._watcher._poll_once()
        self.assertEqual(self.fetch.call_count, 1)
        self.assertEqual(len(self.sent), 2)


class CancelWatcherTests(unittest.TestCase):
    """cancel kind 회귀 — ka10075 폴링으로 원주문이 사라지면 확정."""

    def setUp(self):
        fill_watcher._reset_for_test()
        self.sent = []
        self.fetch_exec = mock.MagicMock(return_value=[])
        self.fetch_open = mock.MagicMock(return_value=[])
        fill_watcher.configure(send_func=lambda msg: self.sent.append(msg) or True,
                                 fetch_executions=self.fetch_exec,
                                 fetch_open_orders=self.fetch_open)

    def _track_cancel(self, new_ord_no='200001', orig_ord_no='100001',
                       cancel_qty=10, started_at=None):
        w = fill_watcher._watcher
        with w._lock:
            w._tracked[new_ord_no] = Tracked(
                ord_no=new_ord_no, account='일반', side='BUY', symbol='005930',
                symbol_name='삼성전자', order_qty=cancel_qty, price=None,
                order_type='CANCEL', card_id='', started_at=started_at or time.time(),
                kind='cancel', orig_ord_no=orig_ord_no,
            )

    def _track_fill(self, ord_no='100001', order_qty=10):
        w = fill_watcher._watcher
        with w._lock:
            w._tracked[ord_no] = Tracked(
                ord_no=ord_no, account='일반', side='BUY', symbol='005930',
                symbol_name='삼성전자', order_qty=order_qty, price=75000,
                order_type='LIMIT', card_id='A7K3', started_at=time.time(),
            )

    def test_cancel_confirmed_when_orig_disappears(self):
        self._track_cancel()
        self.fetch_open.return_value = []  # 원주문 사라짐
        fill_watcher._watcher._poll_once()
        self.assertEqual(len(self.sent), 1)
        self.assertIn('취소 확인', self.sent[0])
        self.assertNotIn('200001', fill_watcher._peek_for_test())

    def test_cancel_confirmed_also_stops_original_fill_watch(self):
        """취소 확정 시 원주문 체결 감시도 같이 끝낸다."""
        self._track_fill(ord_no='100001')
        self._track_cancel(new_ord_no='200001', orig_ord_no='100001')
        self.fetch_open.return_value = []  # 원주문 사라짐 → 취소 확정
        self.fetch_exec.return_value = []  # kt00007 에 취소 상태가 안 잡혀도
        fill_watcher._watcher._poll_once()
        tracked = fill_watcher._peek_for_test()
        self.assertNotIn('200001', tracked)
        self.assertNotIn('100001', tracked)

    def test_cancel_pending_when_orig_still_open(self):
        self._track_cancel()
        self.fetch_open.return_value = [{'ord_no': '100001'}]  # 원주문 아직 미체결
        fill_watcher._watcher._poll_once()
        self.assertEqual(self.sent, [])
        self.assertIn('200001', fill_watcher._peek_for_test())

    def test_cancel_timeout_when_orig_still_open_after_30min(self):
        self._track_cancel(started_at=time.time() - 1801)
        self.fetch_open.return_value = [{'ord_no': '100001'}]
        fill_watcher._watcher._poll_once()
        self.assertEqual(len(self.sent), 1)
        self.assertIn('미확인', self.sent[0])
        self.assertNotIn('200001', fill_watcher._peek_for_test())

    def test_cancel_fetch_error_skips_quietly(self):
        self._track_cancel()
        self.fetch_open.side_effect = RuntimeError('network')
        fill_watcher._watcher._poll_once()
        self.assertEqual(self.sent, [])
        # 추적 유지 (다음 폴링으로 미룸)
        self.assertIn('200001', fill_watcher._peek_for_test())

    def test_executions_not_fetched_when_only_cancel_watches(self):
        self._track_cancel()
        self.fetch_open.return_value = [{'ord_no': '100001'}]
        fill_watcher._watcher._poll_once()
        self.fetch_exec.assert_not_called()
        self.fetch_open.assert_called_once_with('일반')

    def test_open_orders_not_fetched_when_only_fill_watches(self):
        self._track_fill()
        self.fetch_exec.return_value = []
        fill_watcher._watcher._poll_once()
        self.fetch_exec.assert_called_once_with('일반')
        self.fetch_open.assert_not_called()

    def test_user_cancel_suppresses_post_reject_message(self):
        """fill watch 중인 주문에 cancel watch 가 같이 걸려있으면 mdfy_cncl 떠도 사후거절 메시지 X."""
        self._track_fill(ord_no='100001')
        self._track_cancel(new_ord_no='200001', orig_ord_no='100001')
        self.fetch_exec.return_value = [{
            'ord_no': '100001', 'cntr_qty': 0, 'cntr_uv': 0, 'mdfy_cncl': '취소',
        }]
        self.fetch_open.return_value = [{'ord_no': '100001'}]  # 아직 미체결 목록에 있음
        fill_watcher._watcher._poll_once()
        # 사후거절 메시지 억제 — fill watch 만 해제, 취소 확정은 cancel watch 가 별도로 보냄
        self.assertEqual(self.sent, [])
        self.assertNotIn('100001', fill_watcher._peek_for_test())
        self.assertIn('200001', fill_watcher._peek_for_test())

    def test_broker_post_reject_still_alerts_when_no_cancel_watch(self):
        """사용자 cancel 아닌 broker 사후거절은 그대로 알림."""
        self._track_fill(ord_no='100001')
        self.fetch_exec.return_value = [{
            'ord_no': '100001', 'cntr_qty': 0, 'cntr_uv': 0, 'mdfy_cncl': '취소',
        }]
        fill_watcher._watcher._poll_once()
        self.assertEqual(len(self.sent), 1)
        self.assertIn('사후거절', self.sent[0])


class FillWatcherQueueIOTests(unittest.TestCase):
    """큐 파일 read/append/persist."""

    def setUp(self):
        fill_watcher._reset_for_test()
        self.addCleanup(fill_watcher._reset_for_test)

    def test_append_and_read(self):
        e1 = {'ord_no': '100001', 'account': '일반', 'side': 'BUY',
              'symbol': '005930', 'symbol_name': '삼성전자', 'order_qty': 10,
              'price': 75000, 'order_type': 'LIMIT', 'card_id': 'A7K3',
              'started_at': 1.0, 'last_cntr_qty': 0}
        e2 = dict(e1, ord_no='100002', card_id='B2K9')
        fill_watcher.append_queue_entry(e1)
        fill_watcher.append_queue_entry(e2)
        out = fill_watcher.read_queue()
        self.assertEqual(len(out), 2)
        self.assertEqual(out[0]['ord_no'], '100001')
        self.assertEqual(out[1]['ord_no'], '100002')

    def test_read_empty_queue(self):
        self.assertEqual(fill_watcher.read_queue(), [])

    def test_persist_overwrites(self):
        e = {'ord_no': '100001', 'account': '일반', 'side': 'BUY',
             'symbol': '005930', 'symbol_name': '삼성전자', 'order_qty': 10,
             'price': 75000, 'order_type': 'LIMIT', 'card_id': 'A7K3',
             'started_at': 1.0, 'last_cntr_qty': 0}
        fill_watcher.append_queue_entry(e)
        fill_watcher.append_queue_entry(dict(e, ord_no='100002'))
        fill_watcher.persist_queue([dict(e, ord_no='100003')])
        out = fill_watcher.read_queue()
        self.assertEqual(len(out), 1)
        self.assertEqual(out[0]['ord_no'], '100003')

    def test_persist_empty_removes_file(self):
        e = {'ord_no': '100001', 'account': '일반', 'side': 'BUY',
             'symbol': '005930', 'symbol_name': '삼성전자', 'order_qty': 10,
             'price': 75000, 'order_type': 'LIMIT', 'card_id': 'A7K3',
             'started_at': 1.0, 'last_cntr_qty': 0}
        fill_watcher.append_queue_entry(e)
        self.assertTrue(fill_watcher.QUEUE_FILE.exists())
        fill_watcher.persist_queue([])
        self.assertFalse(fill_watcher.QUEUE_FILE.exists())

    def test_read_skips_malformed_lines(self):
        fill_watcher.QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True)
        fill_watcher.QUEUE_FILE.write_text(
            '{"ord_no": "100001", "account": "일반", "side": "BUY", '
            '"symbol": "005930", "symbol_name": "삼성전자", "order_qty": 10, '
            '"price": 75000, "order_type": "LIMIT", "card_id": "A7K3", '
            '"started_at": 1.0, "last_cntr_qty": 0}\n'
            '\n'
            'NOT_JSON_GARBAGE\n',
            encoding='utf-8',
        )
        out = fill_watcher.read_queue()
        self.assertEqual(len(out), 1)


class FillWatcherWatchEntryTests(unittest.TestCase):
    """watch() = 큐 append + 데몬 ensure_running. subprocess.Popen mock."""

    def setUp(self):
        fill_watcher._reset_for_test()
        self.addCleanup(fill_watcher._reset_for_test)
        # subprocess.Popen mock — 실제 데몬 fork 막음
        self.popen = mock.patch.object(fill_watcher.subprocess, 'Popen').start()
        self.addCleanup(mock.patch.stopall)

    def test_watch_appends_to_queue(self):
        fill_watcher.watch(ord_no='100001', account='일반', side='BUY',
                            symbol='005930', symbol_name='삼성전자', order_qty=10,
                            price=75000, order_type='LIMIT', card_id='A7K3')
        out = fill_watcher.read_queue()
        self.assertEqual(len(out), 1)
        self.assertEqual(out[0]['ord_no'], '100001')

    def test_watch_starts_daemon(self):
        fill_watcher.watch(ord_no='100001', account='일반', side='BUY',
                            symbol='005930', symbol_name='삼성전자', order_qty=10,
                            price=75000, order_type='LIMIT', card_id='A7K3')
        self.popen.assert_called_once()
        # 인자 검증 — orders.fill_watcher_daemon 모듈 호출
        args = self.popen.call_args[0][0]
        self.assertEqual(args[1], '-m')
        self.assertEqual(args[2], 'orders.fill_watcher_daemon')

    def test_watch_dedupe_same_ord_no(self):
        fill_watcher.watch(ord_no='100001', account='일반', side='BUY',
                            symbol='005930', symbol_name='삼성전자', order_qty=10,
                            price=75000, order_type='LIMIT', card_id='A7K3')
        fill_watcher.watch(ord_no='100001', account='일반', side='BUY',
                            symbol='005930', symbol_name='삼성전자', order_qty=99,
                            price=99999, order_type='LIMIT', card_id='B2K9')
        out = fill_watcher.read_queue()
        self.assertEqual(len(out), 1)
        # 첫 등록값 유지
        self.assertEqual(out[0]['order_qty'], 10)
        self.assertEqual(out[0]['card_id'], 'A7K3')

    def test_watch_empty_ord_no_ignored(self):
        fill_watcher.watch(ord_no='', account='일반', side='BUY',
                            symbol='005930', symbol_name='삼성전자', order_qty=10,
                            price=75000, order_type='LIMIT', card_id='A7K3')
        self.assertEqual(fill_watcher.read_queue(), [])
        self.popen.assert_not_called()

    def test_watch_cancel_appends_with_kind(self):
        fill_watcher.watch_cancel(new_ord_no='200001', orig_ord_no='100001',
                                    account='일반', side='BUY', symbol='005930',
                                    symbol_name='삼성전자', cancel_qty=7)
        out = fill_watcher.read_queue()
        self.assertEqual(len(out), 1)
        self.assertEqual(out[0]['ord_no'], '200001')
        self.assertEqual(out[0]['orig_ord_no'], '100001')
        self.assertEqual(out[0]['kind'], 'cancel')
        self.assertEqual(out[0]['order_qty'], 7)
        self.popen.assert_called_once()

    def test_watch_cancel_empty_args_ignored(self):
        fill_watcher.watch_cancel(new_ord_no='', orig_ord_no='100001',
                                    account='일반', side='BUY', symbol='005930',
                                    symbol_name='삼성전자', cancel_qty=10)
        fill_watcher.watch_cancel(new_ord_no='200001', orig_ord_no='',
                                    account='일반', side='BUY', symbol='005930',
                                    symbol_name='삼성전자', cancel_qty=10)
        self.assertEqual(fill_watcher.read_queue(), [])
        self.popen.assert_not_called()

    def test_watch_skips_popen_when_daemon_alive(self):
        # 살아있는 데몬 시뮬레이션 — 현재 프로세스 PID 사용
        fill_watcher.PID_FILE.parent.mkdir(parents=True, exist_ok=True)
        fill_watcher.PID_FILE.write_text(str(os.getpid()), encoding='utf-8')
        fill_watcher.watch(ord_no='100001', account='일반', side='BUY',
                            symbol='005930', symbol_name='삼성전자', order_qty=10,
                            price=75000, order_type='LIMIT', card_id='A7K3')
        self.popen.assert_not_called()
        self.assertEqual(len(fill_watcher.read_queue()), 1)


class FillWatcherDaemonAliveTests(unittest.TestCase):
    """is_daemon_alive — PID 파일 stale 검출."""

    def setUp(self):
        fill_watcher._reset_for_test()
        self.addCleanup(fill_watcher._reset_for_test)

    def test_no_pid_file(self):
        self.assertFalse(fill_watcher.is_daemon_alive())

    def test_invalid_pid_file(self):
        fill_watcher.PID_FILE.parent.mkdir(parents=True, exist_ok=True)
        fill_watcher.PID_FILE.write_text('not_a_number', encoding='utf-8')
        self.assertFalse(fill_watcher.is_daemon_alive())

    def test_stale_pid(self):
        # 절대 안 쓰일 큰 PID — Pid lookup 실패
        fill_watcher.PID_FILE.parent.mkdir(parents=True, exist_ok=True)
        fill_watcher.PID_FILE.write_text('99999999', encoding='utf-8')
        self.assertFalse(fill_watcher.is_daemon_alive())

    def test_alive_pid(self):
        # 현재 프로세스는 살아있음
        fill_watcher.PID_FILE.parent.mkdir(parents=True, exist_ok=True)
        fill_watcher.PID_FILE.write_text(str(os.getpid()), encoding='utf-8')
        self.assertTrue(fill_watcher.is_daemon_alive())


class FillWatcherSyncFromQueueTests(unittest.TestCase):
    """sync_from_queue — 큐 → _tracked 양방향 동기화."""

    def setUp(self):
        fill_watcher._reset_for_test()
        self.addCleanup(fill_watcher._reset_for_test)

    def _entry(self, ord_no='100001', last_cntr=0):
        return {'ord_no': ord_no, 'account': '일반', 'side': 'BUY',
                'symbol': '005930', 'symbol_name': '삼성전자', 'order_qty': 10,
                'price': 75000, 'order_type': 'LIMIT', 'card_id': 'A7K3',
                'started_at': 1.0, 'last_cntr_qty': last_cntr}

    def test_sync_adds_new_entries(self):
        fill_watcher._watcher.sync_from_queue([self._entry('100001'), self._entry('100002')])
        self.assertEqual(set(fill_watcher._peek_for_test().keys()), {'100001', '100002'})

    def test_sync_removes_missing(self):
        fill_watcher._watcher.sync_from_queue([self._entry('100001'), self._entry('100002')])
        fill_watcher._watcher.sync_from_queue([self._entry('100001')])
        self.assertEqual(set(fill_watcher._peek_for_test().keys()), {'100001'})

    def test_sync_preserves_progress(self):
        fill_watcher._watcher.sync_from_queue([self._entry('100001', last_cntr=3)])
        self.assertEqual(fill_watcher._peek_for_test()['100001'].last_cntr_qty, 3)

    def test_snapshot_round_trip(self):
        fill_watcher._watcher.sync_from_queue([self._entry('100001', last_cntr=3)])
        snap = fill_watcher._watcher.snapshot_entries()
        self.assertEqual(len(snap), 1)
        self.assertEqual(snap[0]['ord_no'], '100001')
        self.assertEqual(snap[0]['last_cntr_qty'], 3)


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