'''Unit tests for Median.'''
import unittest
import numpy as np
from QVideo.filters.Median import Median


_SHAPE = (4, 4)
_A = np.full(_SHAPE, 10, dtype=np.uint8)
_B = np.full(_SHAPE, 20, dtype=np.uint8)
_C = np.full(_SHAPE, 30, dtype=np.uint8)


def make_filter(**kwargs) -> Median:
    return Median(**kwargs)


class TestMedian(unittest.TestCase):

    def test_default_order(self):
        f = make_filter()
        self.assertEqual(f.order, 1)

    def test_custom_order(self):
        f = make_filter(order=2)
        self.assertEqual(f.order, 2)

    def test_data_none_before_add(self):
        f = make_filter()
        self.assertIsNone(f.get())

    def test_shape_none_before_add(self):
        f = make_filter()
        self.assertIsNone(f.shape)

    def test_not_ready_before_three_frames(self):
        f = make_filter()
        f.add(_A)
        f.add(_B)
        self.assertFalse(f.ready())

    def test_ready_after_three_frames(self):
        f = make_filter()
        f.add(_A)
        f.add(_B)
        f.add(_C)
        self.assertTrue(f.ready())

    def test_median_of_three_constant_frames(self):
        f = make_filter()
        frame = np.full(_SHAPE, 50, dtype=np.uint8)
        for _ in range(3):
            f.add(frame)
        result = f.get()
        np.testing.assert_array_equal(result, frame)

    def test_median_of_three_picks_middle_value(self):
        f = make_filter()
        f.add(_A)   # 10
        f.add(_B)   # 20
        f.add(_C)   # 30 → median is 20
        result = f.get()
        np.testing.assert_array_equal(result, _B)

    def test_get_does_not_reset_ready_flag(self):
        f = make_filter()
        for frame in (_A, _B, _C):
            f.add(frame)
        self.assertTrue(f.ready())
        f.get()
        self.assertTrue(f.ready())

    def test_ready_resets_on_next_add(self):
        f = make_filter()
        for frame in (_A, _B, _C):
            f.add(frame)
        self.assertTrue(f.ready())
        f.add(_A)  # new add() resets ready at its start
        self.assertFalse(f.ready())

    def test_shape_set_after_first_add(self):
        f = make_filter()
        f.add(_A)
        self.assertEqual(f.shape, _SHAPE)

    def test_shape_change_reinitializes(self):
        f = make_filter()
        f.add(_A)
        new_frame = np.zeros((8, 8), dtype=np.uint8)
        f.add(new_frame)
        self.assertEqual(f.shape, (8, 8))

    def test_seed_data_sets_shape(self):
        f = make_filter(data=_A)
        self.assertEqual(f.shape, _SHAPE)

    def test_order_setter_reinitializes(self):
        f = make_filter(order=1)
        f.add(_A)
        f.order = 2
        self.assertIsNone(f.shape)

    def test_order_setter_same_value_is_noop(self):
        f = make_filter(order=1)
        f.add(_A)
        f.order = 1
        self.assertEqual(f.shape, _SHAPE)

    def test_reset_clears_ready(self):
        f = make_filter(data=_A)
        for frame in (_A, _B, _C):
            f.add(frame)
        f.reset()
        self.assertFalse(f.ready())

    def test_reset_clears_index(self):
        f = make_filter(data=_A)
        for frame in (_A, _B, _C):
            f.add(frame)
        f.reset()
        self.assertEqual(f._index, 0)

    def test_reset_zeros_buffers(self):
        f = make_filter(data=_A)
        f.add(_A)
        f.reset()
        np.testing.assert_array_equal(f._result, np.zeros(_SHAPE, dtype=np.uint8))
        np.testing.assert_array_equal(f._buffer, np.zeros((2, *_SHAPE), dtype=np.uint8))

    def test_clear_forgets_shape(self):
        f = make_filter(data=_A)
        f._clear()
        self.assertIsNone(f.shape)

    def test_clear_sets_result_to_none(self):
        f = make_filter(data=_A)
        f._clear()
        self.assertIsNone(f._result)

    def test_order_setter_clears_state(self):
        f = make_filter(order=1, data=_A)
        f.order = 2
        self.assertIsNone(f.shape)
        self.assertIsNone(f._result)

    def test_call_returns_result(self):
        f = make_filter()
        for frame in (_A, _B, _C):
            f(frame)
        result = f(_C)
        self.assertIsNotNone(result)

    def test_does_not_mutate_input(self):
        f = make_filter()
        original = _A.copy()
        f.add(_A)
        np.testing.assert_array_equal(_A, original)


class TestMedianOrder2(unittest.TestCase):
    '''Tests for order=2 median — exercises the _next recursive branch.

    An order=2 filter wraps an order=1 sub-filter.  The sub-filter
    produces its first result after 3 frames, then one more every 2
    additional frames (3, 5, 7, …).  The outer filter needs 3
    sub-results, so the first outer result arrives after 7 frames.
    '''

    def test_not_ready_before_seven_frames(self):
        f = make_filter(order=2)
        for _ in range(6):
            f.add(_A)
        self.assertFalse(f.ready())

    def test_ready_after_seven_frames(self):
        f = make_filter(order=2)
        for _ in range(7):
            f.add(_A)
        self.assertTrue(f.ready())

    def test_order2_constant_input_returns_that_value(self):
        f = make_filter(order=2)
        for _ in range(7):
            f.add(_B)
        np.testing.assert_array_equal(f.get(), _B)

    def test_next_not_ready_causes_early_return(self):
        '''Adding fewer than three frames leaves _next not ready → early return.'''
        f = make_filter(order=2)
        f.add(_A)   # _next.add(), _next not ready → return before outer logic
        f.add(_B)   # same
        # Neither outer add produced a result; outer index must still be 0.
        self.assertEqual(f._index, 0)

    def test_next_ready_feeds_result_to_outer(self):
        '''After the sub-estimator is ready its result propagates to the outer level.'''
        f = make_filter(order=2)
        for frame in (_A, _A, _A):   # three identical → sub-median = A, _next ready
            f.add(frame)
        # _next is now ready; the outer level consumed that result and
        # advanced its own index to 1.
        self.assertEqual(f._index, 1)


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