import threading
import time
import unittest

from app.tracking_kernel import TrackingKernel


class TrackingKernelTest(unittest.TestCase):
    def wait(self, predicate):
        end = time.monotonic() + 2
        while not predicate():
            if time.monotonic() >= end:
                self.fail('Worker did not reach expected state.')
            time.sleep(.001)

    def test_fifo_and_nonoverlap(self):
        gate = TrackingKernel()
        entered, release = threading.Event(), threading.Event()
        order, errors = [], []
        def worker(index):
            try:
                with gate.turn():
                    order.append(index)
                    if index == 0:
                        entered.set()
                        if not release.wait(2):
                            raise RuntimeError('Fixture release timeout')
            except Exception as error:
                errors.append(error)
        threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)]
        try:
            threads[0].start()
            self.assertTrue(entered.wait(2))
            threads[1].start()
            self.wait(lambda: len(gate._waiting) == 2)
            threads[2].start()
            self.wait(lambda: len(gate._waiting) == 3)
            self.assertEqual(order, [0])
        finally:
            release.set()
            for thread in threads:
                if thread.ident is not None:
                    thread.join(2)
        self.assertEqual(order, [0, 1, 2])
        self.assertEqual(errors, [])
        self.assertEqual(len(gate._waiting), 0)

    def test_exception_and_reentrancy_release_turn(self):
        gate = TrackingKernel()
        with self.assertRaises(ValueError):
            with gate.turn():
                raise ValueError('test')
        with gate.turn():
            with self.assertRaises(RuntimeError):
                with gate.turn():
                    pass
        with gate.turn() as wait_ns:
            self.assertGreaterEqual(wait_ns, 0)
        self.assertEqual(len(gate._waiting), 0)

    def test_wait_failure_removes_only_own_ticket(self):
        from unittest.mock import patch
        gate = TrackingKernel()
        with patch.object(gate._condition, 'wait_for', side_effect=RuntimeError('wait failed')):
            with self.assertRaises(RuntimeError):
                with gate.turn():
                    pass
        with gate.turn():
            self.assertEqual(len(gate._waiting), 1)
        self.assertEqual(len(gate._waiting), 0)
