from __future__ import annotations

import json
import os
import time
import threading

import pytest

from pulselog.levels import Level
from pulselog.logger import Logger
from pulselog.queue import LogQueue
from pulselog.worker import BackgroundWorker
from pulselog.record import LogRecord
from pulselog.checkpoint import CheckpointStore


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


class TestEndToEnd:
    """Integration tests: logger → queue → worker → handler chain."""

    def setup_method(self):
        os.environ["CI"] = "true"

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

    def test_logger_to_handler_pipeline(self):
        """Full pipeline: Logger → queue → worker → handler receives record."""
        received = []
        logger = Logger("e2e", dashboard=False)
        logger._worker._handlers.append(received.extend)

        logger.info("pipeline test", key="val")
        logger.flush()
        time.sleep(0.1)

        assert any(r.msg == "pipeline test" for r in received)
        extra_records = [r for r in received if r.msg == "pipeline test"]
        assert extra_records[0].extra.get("key") == "val"
        logger.shutdown()

    def test_queue_worker_handler(self):
        """Direct queue → worker → handler without Logger wrapper."""
        q = LogQueue(maxsize=100)
        received = []
        worker = BackgroundWorker(q, handlers=[received.extend], interval=0.05)
        worker.start()

        for i in range(10):
            q.put(make_record(f"direct {i}"))

        worker.shutdown()

        msgs = [r.msg for r in received]
        assert len(msgs) == 10
        for i in range(10):
            assert f"direct {i}" in msgs

    def test_checkpoint_persists_in_logger(self):
        """save → load → delete lifecycle through Logger."""
        logger = Logger("cp-e2e", dashboard=False)
        logger.save("stage1", {"epoch": 5, "loss": 0.12}, status="DONE", progress=50)
        result = logger.load("stage1")
        assert result["epoch"] == 5
        assert result["_meta"]["status"] == "DONE"
        assert result["_meta"]["progress"] == 50

        logger.delete_checkpoint("stage1")
        assert logger.load("stage1") is None
        logger.shutdown()

    def test_multiple_loggers_independent(self):
        """Two loggers must not share queues, workers, or checkpoint stores."""
        r1, r2 = [], []
        l1 = Logger("app1", dashboard=False)
        l2 = Logger("app2", dashboard=False)
        l1._worker._handlers.append(r1.extend)
        l2._worker._handlers.append(r2.extend)

        l1.info("from-l1")
        l2.info("from-l2")
        l1.flush(); l2.flush()
        time.sleep(0.1)

        assert any(r.msg == "from-l1" for r in r1)
        assert not any(r.msg == "from-l1" for r in r2)
        assert any(r.msg == "from-l2" for r in r2)
        l1.shutdown(); l2.shutdown()

    def test_record_to_dict_serializable(self):
        """LogRecord.to_dict() must produce JSON-serializable output."""
        record = LogRecord(
            level=Level.WARNING,
            msg="test serialization",
            timestamp=time.time(),
            extra={"user": "bob", "count": 3},
            tag="my-tag",
        )
        d = record.to_dict()
        # Must not raise
        serialized = json.dumps(d)
        parsed = json.loads(serialized)
        assert parsed["level"] == "WARNING"
        assert parsed["msg"] == "test serialization"
        assert parsed["extra"]["user"] == "bob"
        assert parsed["tag"] == "my-tag"

    def test_flush_before_shutdown_no_loss(self):
        """Records enqueued right before shutdown must not be lost."""
        received = []
        logger = Logger("flush-test", dashboard=False)
        logger._worker._handlers.append(received.extend)

        for i in range(50):
            logger.info(f"pre-shutdown {i}")

        logger.flush()
        logger.shutdown()

        msgs = [r.msg for r in received if r.msg.startswith("pre-shutdown")]
        assert len(msgs) == 50

    def test_worker_handler_exception_isolates(self):
        """A crashing handler must not affect other handlers."""
        received = []

        def bad(batch):
            raise RuntimeError("intentional failure")

        logger = Logger("resilience", dashboard=False)
        logger._worker._handlers.insert(0, bad)
        logger._worker._handlers.append(received.extend)

        logger.info("should survive")
        logger.flush()
        time.sleep(0.1)

        assert any(r.msg == "should survive" for r in received)
        assert logger._worker.is_alive()
        logger.shutdown()

    def test_stats_reflect_activity(self):
        logger = Logger("stats-test", dashboard=False)
        logger.info("a")
        logger.info("b")
        logger.save("chk", {"x": 1})
        logger.flush()
        time.sleep(0.1)

        s = logger.stats()
        assert s["records_logged"] >= 3  # 2 infos + 1 checkpoint log
        assert s["checkpoints_saved"] == 1
        assert isinstance(s["uptime_seconds"], float)
        logger.shutdown()

    def test_tag_appears_on_subsequent_records(self):
        received = []
        logger = Logger("tag-test", dashboard=False)
        logger._worker._handlers.append(received.extend)

        logger.tag("phase-1")
        logger.info("tagged-msg")
        logger.flush()
        time.sleep(0.1)

        tagged = [r for r in received if r.msg == "tagged-msg"]
        assert tagged and tagged[0].tag == "phase-1"
        logger.shutdown()

    def test_level_filter_at_critical(self):
        received = []
        logger = Logger("crit-test", dashboard=False, level="CRITICAL")
        logger._worker._handlers.append(received.extend)

        logger.debug("d"); logger.info("i"); logger.warning("w"); logger.error("e")
        logger.critical("boom")
        logger.flush()
        time.sleep(0.1)

        msgs = [r.msg for r in received]
        assert msgs == ["boom"]
        logger.shutdown()
