from __future__ import annotations

import os
import time

import pytest

from pulselog.logger import Logger
from pulselog.levels import Level


class TestLogger:
    def setup_method(self):
        # Avoid browser open and no dashboard overhead in tests
        os.environ["CI"] = "true"
        self.logger = Logger("test", dashboard=False)

    def teardown_method(self):
        self.logger.shutdown()
        os.environ.pop("CI", None)

    def test_logger_full_pipeline(self):
        received = []
        self.logger._worker._handlers.append(received.extend)
        self.logger.info("hello world", user="alice")
        self.logger.flush()
        time.sleep(0.1)
        assert any(r.msg == "hello world" for r in received)
        assert any(r.extra.get("user") == "alice" for r in received)

    def test_level_filtering_debug(self):
        received = []
        logger = Logger("filter-test", dashboard=False, level="WARNING")
        logger._worker._handlers.append(received.extend)
        logger.debug("ignored")
        logger.info("also ignored")
        logger.warning("captured")
        logger.flush()
        time.sleep(0.1)
        msgs = [r.msg for r in received]
        assert "ignored" not in msgs
        assert "also ignored" not in msgs
        assert "captured" in msgs
        logger.shutdown()

    def test_all_log_levels_emitted(self):
        received = []
        logger = Logger("all-levels", dashboard=False, level="DEBUG")
        logger._worker._handlers.append(received.extend)
        logger.debug("d")
        logger.info("i")
        logger.warning("w")
        logger.error("e")
        logger.critical("c")
        logger.flush()
        time.sleep(0.1)
        levels = {r.level.name for r in received}
        assert levels == {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
        logger.shutdown()

    def test_checkpoint_save_load(self):
        self.logger.save("s1", {"val": 42}, status="DONE", progress=100)
        result = self.logger.load("s1")
        assert result is not None
        assert result["val"] == 42

    def test_checkpoint_load_missing_returns_none(self):
        assert self.logger.load("ghost") is None

    def test_checkpoints_list(self):
        self.logger.save("a", {})
        self.logger.save("b", {})
        names = self.logger.checkpoints()
        assert "a" in names
        assert "b" in names

    def test_delete_checkpoint(self):
        self.logger.save("del_me", {"x": 1})
        self.logger.delete_checkpoint("del_me")
        assert self.logger.load("del_me") is None

    def test_stats(self):
        self.logger.info("one")
        self.logger.info("two")
        self.logger.flush()
        stats = self.logger.stats()
        assert stats["records_logged"] >= 2
        assert "queue_size" in stats
        assert "uptime_seconds" in stats
        assert stats["uptime_seconds"] > 0

    def test_tag_sets_current_tag(self):
        received = []
        self.logger._worker._handlers.append(received.extend)
        self.logger.tag("my-stage")
        self.logger.info("tagged msg")
        self.logger.flush()
        time.sleep(0.1)
        tagged = [r for r in received if r.msg == "tagged msg"]
        assert tagged and tagged[0].tag == "my-stage"

    def test_no_threads_when_dashboard_false(self):
        import threading
        before = {t.name for t in threading.enumerate()}
        logger = Logger("no-dash", dashboard=False)
        after = {t.name for t in threading.enumerate()}
        new_threads = after - before
        dashboard_threads = {n for n in new_threads if "server" in n.lower()}
        assert not dashboard_threads, f"Dashboard threads started: {dashboard_threads}"
        logger.shutdown()

    def test_throughput_million_records(self):
        """1M logger calls must complete in under 15 seconds.

        Production target is <2s; we allow 15s here because pytest-cov adds
        ~5-8x instrumentation overhead per call.  Bare runtime is ~2-3µs/call.
        """
        logger = Logger("perf", dashboard=False, level="DEBUG")
        start = time.perf_counter()
        for i in range(1_000_000):
            logger.info(f"msg {i}")
        elapsed = time.perf_counter() - start
        logger.shutdown()
        assert elapsed < 15.0, f"1M records took {elapsed:.2f}s — too slow even under coverage"

    def test_divider_method(self):
        received = []
        self.logger._worker._handlers.append(received.extend)
        self.logger.divider("section break")
        self.logger.divider()   # no label
        self.logger.flush()
        time.sleep(0.1)
        dividers = [r for r in received if r.extra.get("_divider")]
        assert len(dividers) >= 2

    def test_logger_with_dashboard(self):
        """Exercise the dashboard=True code path."""
        logger = Logger("dash-test", dashboard=True, auto_open=False, port=19234)
        time.sleep(0.4)
        s = logger.stats()
        assert s["dashboard_clients"] == 0
        logger.shutdown()

    def test_atexit_handler_does_not_raise(self):
        """_atexit_handler must never raise, even after shutdown."""
        logger = Logger("atexit-test", dashboard=False)
        logger.shutdown()
        logger._atexit_handler()  # double-call must be safe

    def test_shutdown_with_server(self):
        """Shutdown calls server.shutdown() when server exists."""
        logger = Logger("srv-shut", dashboard=True, auto_open=False, port=19235)
        time.sleep(0.3)
        logger.shutdown()  # must not raise

    def test_level_from_str_invalid(self):
        from pulselog.levels import Level
        with pytest.raises(ValueError, match="Unknown log level"):
            Level.from_str("TURBO")
