from __future__ import annotations

import time
import threading

import pytest

from pulselog.levels import Level
from pulselog.queue import LogQueue
from pulselog.record import LogRecord
from pulselog.worker import BackgroundWorker


def make_record(msg: str = "test") -> LogRecord:
    return LogRecord(level=Level.INFO, msg=msg, timestamp=time.time(), extra={})


class TestBackgroundWorker:
    def setup_method(self):
        self.queue = LogQueue(maxsize=1000)
        self.worker = None

    def teardown_method(self):
        if self.worker and self.worker.is_alive():
            self.worker.shutdown()

    def test_handler_called_with_batch(self):
        received = []
        self.worker = BackgroundWorker(self.queue, handlers=[received.extend], interval=0.05)
        self.worker.start()
        self.queue.put(make_record("hello"))
        time.sleep(0.15)
        assert any(r.msg == "hello" for r in received)

    def test_handler_exception_does_not_crash_worker(self):
        def bad_handler(batch):
            raise RuntimeError("boom")

        self.worker = BackgroundWorker(self.queue, handlers=[bad_handler], interval=0.05)
        self.worker.start()
        self.queue.put(make_record("test"))
        time.sleep(0.15)
        assert self.worker.is_alive(), "Worker crashed due to handler exception"

    def test_multiple_handlers_all_called(self):
        received_a, received_b = [], []
        self.worker = BackgroundWorker(
            self.queue,
            handlers=[received_a.extend, received_b.extend],
            interval=0.05,
        )
        self.worker.start()
        self.queue.put(make_record("multi"))
        time.sleep(0.15)
        assert any(r.msg == "multi" for r in received_a)
        assert any(r.msg == "multi" for r in received_b)

    def test_shutdown_drains_remaining(self):
        received = []
        self.worker = BackgroundWorker(self.queue, handlers=[received.extend], interval=1.0)
        self.worker.start()
        self.queue.put(make_record("last"))
        # shutdown should trigger a final drain
        self.worker.shutdown()
        assert any(r.msg == "last" for r in received)

    def test_worker_is_daemon(self):
        self.worker = BackgroundWorker(self.queue, handlers=[], interval=0.05)
        assert self.worker.daemon

    def test_none_handlers_are_skipped(self):
        """None in handlers list should not cause crash."""
        received = []
        self.worker = BackgroundWorker(
            self.queue, handlers=[None, received.extend], interval=0.05  # type: ignore
        )
        self.worker.start()
        self.queue.put(make_record("ok"))
        time.sleep(0.15)
        assert any(r.msg == "ok" for r in received)

    def test_high_throughput_no_loss(self):
        """Worker must process all 500 records eventually."""
        received = []
        lock = threading.Lock()

        def handler(batch):
            with lock:
                received.extend(batch)

        self.worker = BackgroundWorker(self.queue, handlers=[handler], interval=0.01)
        self.worker.start()
        for i in range(500):
            self.queue.put(make_record(f"r{i}"))
        self.worker.shutdown()

        with lock:
            assert len(received) == 500
