import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch

from app import multi_pipeline as mp
from app.native_metadata import NativeFrame, NativeMetadataError
from app.stream_lifecycle import RoleRunHandle


class ReceiverReturnTest(unittest.TestCase):
    def setUp(self):
        self.frame = SimpleNamespace(buffer_pts=0, frame_number=0, source_width=160, source_height=90)
        self.buffer = SimpleNamespace(batch_meta=SimpleNamespace(frame_items=[self.frame]), extract=Mock())
        self.path = SimpleNamespace(ingress=Mock(), visual=None, submit=Mock())
        self.handle = RoleRunHandle('det', 1, Mock())
        self.state = {'n': 0, 'pipe': Mock()}
        self.native = NativeFrame(0, 0, 160, 90, (), 160, 90)

    def receiver(self, maximum=0, current=lambda: True):
        return mp._make_retriever('det', 'unused', 'unused', Mock(), maximum, self.state,
            component_id=7, role_handle=self.handle, result_path=self.path, is_current=current)()

    def test_empty_detection_is_consumed_once(self):
        with patch.object(mp, 'copy_detection_frame', return_value=self.native):
            self.assertEqual(self.receiver().consume(self.buffer), 1)
        self.path.submit.assert_called_once()
        self.buffer.extract.assert_not_called()
        self.assertEqual(self.state['n'], 1)
        self.assertIsNone(self.handle.failure)

    def test_stale_buffer_is_discarded_without_retry(self):
        with patch.object(mp, 'copy_detection_frame') as copy:
            self.assertEqual(self.receiver(current=lambda: False).consume(self.buffer), 1)
        copy.assert_not_called()
        self.path.submit.assert_not_called()
        self.assertEqual(self.state['n'], 0)

    def test_stopped_buffer_is_discarded_without_retry(self):
        self.handle.stop()
        with patch.object(mp, 'copy_detection_frame') as copy:
            self.assertEqual(self.receiver().consume(self.buffer), 1)
        copy.assert_not_called()
        self.path.submit.assert_not_called()

    def test_stop_during_copy_discards_without_submit(self):
        def copy(*args):
            self.handle.stop()
            return self.native
        with patch.object(mp, 'copy_detection_frame', side_effect=copy):
            self.assertEqual(self.receiver().consume(self.buffer), 1)
        self.path.submit.assert_not_called()

    def test_decode_error_returns_negative_and_preserves_failure(self):
        with patch.object(mp, 'copy_detection_frame', side_effect=NativeMetadataError('COMPLETION_INVALID')):
            self.assertEqual(self.receiver().consume(self.buffer), -1)
        self.assertEqual(self.handle.failure.failure_stage, 'native_metadata')
        self.path.submit.assert_not_called()

    def test_submit_error_is_not_success(self):
        self.path.submit.side_effect = RuntimeError('test')
        with patch.object(mp, 'copy_detection_frame', return_value=self.native):
            self.assertEqual(self.receiver().consume(self.buffer), -1)
        self.assertIsNotNone(self.handle.failure)

    def test_finite_limit_schedules_stop_only_once(self):
        receiver = self.receiver(maximum=1)
        with patch.object(mp, 'copy_detection_frame', return_value=self.native) as copy, \
                patch.object(mp.GLib, 'timeout_add') as timer:
            self.assertEqual(receiver.consume(self.buffer), 1)
            self.assertEqual(receiver.consume(self.buffer), 1)
            self.assertEqual(receiver.consume(self.buffer), 1)
        copy.assert_called_once()
        self.path.submit.assert_called_once()
        timer.assert_called_once()
        self.assertEqual(self.state['n'], 1)
        self.assertFalse(timer.call_args.args[1]())
        self.state['pipe'].stop.assert_called_once()

    def test_empty_batch_still_consumes_buffer(self):
        self.buffer.batch_meta.frame_items = []
        self.assertEqual(self.receiver().consume(self.buffer), 1)
        self.path.submit.assert_not_called()
