import asyncio
import datetime
import logging
import pickle

import pytest

from loguru import logger

from .conftest import parse


def print_(message):
    print(message, end="")


async def async_print(msg):
    print_(msg)


class StreamHandler:
    def __init__(self, flushable=False, stoppable=False):
        if flushable:
            self.flush = self._flush
        if stoppable:
            self.stop = self._stop

        self.wrote = ""
        self.flushed = False
        self.stopped = False

    def write(self, message):
        self.wrote += message

    def _flush(self):
        self.flushed = True

    def _stop(self):
        self.stopped = True


class StandardHandler(logging.Handler):
    def __init__(self, level):
        super().__init__(level)
        self.written = ""
        self.lock = None

    def emit(self, record):
        self.written += record.getMessage()

    def acquire(self):
        pass

    def createLock(self):
        return None


def format_function(record):
    return "-> <red>{message}</red>"


def filter_function(record):
    return "[PASS]" in record["message"]


def patch_function(record):
    record["extra"]["foo"] = "bar"


def rotation_function(message, file):
    pass


def retention_function(files):
    pass


def compression_function(path):
    pass


def test_pickling_function_handler(capsys):
    logger.add(print_, format="{level} - {function} - {message}")
    pickled = pickle.dumps(logger)
    unpikcled = pickle.loads(pickled)
    unpikcled.debug("A message")
    out, err = capsys.readouterr()
    assert out == "DEBUG - test_pickling_function_handler - A message\n"
    assert err == ""


def test_pickling_coroutine_function_handler(capsys):
    logger.add(async_print, format="{level} - {function} - {message}")
    pickled = pickle.dumps(logger)
    unpikcled = pickle.loads(pickled)

    async def async_debug():
        unpikcled.debug("A message")
        await unpikcled.complete()

    asyncio.run(async_debug())

    out, err = capsys.readouterr()
    assert out == "DEBUG - async_debug - A message\n"
    assert err == ""


@pytest.mark.parametrize("flushable", [True, False])
@pytest.mark.parametrize("stoppable", [True, False])
def test_pickling_stream_handler(flushable, stoppable):
    stream = StreamHandler(flushable, stoppable)
    i = logger.add(stream, format="{level} - {function} - {message}")
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.debug("A message")
    stream = next(iter(unpickled._core.handlers.values()))._sink._stream
    unpickled.remove(i)
    assert stream.wrote == "DEBUG - test_pickling_stream_handler - A message\n"
    assert stream.flushed == flushable
    assert stream.stopped == stoppable


def test_pickling_standard_handler():
    handler = StandardHandler(logging.NOTSET)
    logger.add(handler, format="{level} - {function} - {message}")
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.debug("A message")
    handler = next(iter(unpickled._core.handlers.values()))._sink._handler
    assert handler.written == "DEBUG - test_pickling_standard_handler - A message"


def test_pickling_standard_handler_root_logger_not_picklable(monkeypatch, capsys):
    def __reduce__():
        raise TypeError("Not picklable")

    monkeypatch.setattr(logging.getLogger(), "__reduce__", __reduce__, raising=False)

    handler = StandardHandler(logging.NOTSET)
    logger.add(handler, format="=> {message}", catch=False)

    pickled = pickle.dumps(logger)
    pickle.loads(pickled)

    logger.info("Ok")
    out, err = capsys.readouterr()
    assert out == ""
    assert err == ""
    assert handler.written == "=> Ok"


def test_pickling_file_handler(tmpdir):
    file = tmpdir.join("test.log")
    logger.add(str(file), format="{level} - {function} - {message}", delay=True)
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.debug("A message")
    assert file.read() == "DEBUG - test_pickling_file_handler - A message\n"


@pytest.mark.parametrize(
    "rotation",
    [
        1000,
        "daily",
        datetime.timedelta(minutes=60),
        datetime.time(hour=12, minute=00, second=00),
        "200 MB",
        "10:00",
        "5 hours",
        rotation_function,
    ],
)
def test_pickling_file_handler_rotation(tmpdir, rotation):
    file = tmpdir.join("test.log")
    logger.add(str(file), format="{level} - {function} - {message}", delay=True, rotation=rotation)
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.debug("A message")
    assert file.read() == "DEBUG - test_pickling_file_handler_rotation - A message\n"


@pytest.mark.parametrize(
    "retention", [1000, datetime.timedelta(hours=13), "10 days", retention_function]
)
def test_pickling_file_handler_retention(tmpdir, retention):
    file = tmpdir.join("test.log")
    logger.add(
        str(file), format="{level} - {function} - {message}", delay=True, retention=retention
    )
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.debug("A message")
    assert file.read() == "DEBUG - test_pickling_file_handler_retention - A message\n"


@pytest.mark.parametrize("compression", ["zip", "gz", "tar", compression_function])
def test_pickling_file_handler_compression(tmpdir, compression):
    file = tmpdir.join("test.log")
    logger.add(
        str(file), format="{level} - {function} - {message}", delay=True, compression=compression
    )
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.debug("A message")
    assert file.read() == "DEBUG - test_pickling_file_handler_compression - A message\n"


def test_pickling_no_handler(writer):
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.add(writer, format="{level} - {function} - {message}")
    unpickled.debug("A message")
    assert writer.read() == "DEBUG - test_pickling_no_handler - A message\n"


def test_pickling_handler_not_serializable():
    logger.add(lambda m: None)
    with pytest.raises((pickle.PicklingError, AttributeError), match="Can't (pickle|get local)"):
        pickle.dumps(logger)


def test_pickling_filter_function(capsys):
    logger.add(print_, format="{message}", filter=filter_function)
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.info("Nope")
    unpickled.info("[PASS] Yes")
    out, err = capsys.readouterr()
    assert out == "[PASS] Yes\n"
    assert err == ""


@pytest.mark.parametrize("filter", ["", "tests"])
def test_pickling_filter_name(capsys, filter):
    logger.add(print_, format="{message}", filter=filter)
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.info("A message")
    out, err = capsys.readouterr()
    assert out == "A message\n"
    assert err == ""


@pytest.mark.parametrize("colorize", [True, False])
def test_pickling_format_string(capsys, colorize):
    logger.add(print_, format="-> <red>{message}</red>", colorize=colorize)
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.info("The message")
    out, err = capsys.readouterr()
    assert out == parse("-> <red>The message</red>\n", strip=not colorize)
    assert err == ""


@pytest.mark.parametrize("colorize", [True, False])
def test_pickling_format_function(capsys, colorize):
    logger.add(print_, format=format_function, colorize=colorize)
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.info("The message")
    out, err = capsys.readouterr()
    assert out == parse("-> <red>The message</red>", strip=not colorize)
    assert err == ""


def test_pickling_filter_function_not_serializable():
    logger.add(print, filter=lambda r: True)
    with pytest.raises((pickle.PicklingError, AttributeError), match="Can't (pickle|get local)"):
        pickle.dumps(logger)


def test_pickling_format_function_not_serializable():
    logger.add(print, format=lambda r: "{message}")
    with pytest.raises((pickle.PicklingError, AttributeError), match="Can't (pickle|get local)"):
        pickle.dumps(logger)


def test_pickling_bound_logger(writer):
    bound_logger = logger.bind(foo="bar")
    pickled = pickle.dumps(bound_logger)
    unpickled = pickle.loads(pickled)
    unpickled.add(writer, format="{extra[foo]}")
    unpickled.info("Test")
    assert writer.read() == "bar\n"


def test_pickling_patched_logger(writer):
    patched_logger = logger.patch(patch_function)
    pickled = pickle.dumps(patched_logger)
    unpickled = pickle.loads(pickled)
    unpickled.add(writer, format="{extra[foo]}")
    unpickled.info("Test")
    assert writer.read() == "bar\n"


def test_remove_after_pickling(capsys):
    i = logger.add(print_, format="{message}")
    logger.info("A")
    pickled = pickle.dumps(logger)
    unpickled = pickle.loads(pickled)
    unpickled.remove(i)
    unpickled.info("B")
    out, err = capsys.readouterr()
    assert out == "A\n"
    assert err == ""


def test_pickling_logging_method(capsys):
    logger.add(print_, format="{level} - {function} - {message}")
    pickled = pickle.dumps(logger.critical)
    func = pickle.loads(pickled)
    func("A message")
    out, err = capsys.readouterr()
    assert out == "CRITICAL - test_pickling_logging_method - A message\n"
    assert err == ""


def test_pickling_log_method(capsys):
    logger.add(print_, format="{level} - {function} - {message}")
    pickled = pickle.dumps(logger.log)
    func = pickle.loads(pickled)
    func(19, "A message")
    out, err = capsys.readouterr()
    assert out == "Level 19 - test_pickling_log_method - A message\n"
    assert err == ""


@pytest.mark.parametrize(
    "method",
    [
        logger.add,
        logger.remove,
        logger.catch,
        logger.opt,
        logger.bind,
        logger.patch,
        logger.level,
        logger.disable,
        logger.enable,
        logger.configure,
        logger.parse,
        logger.exception,
    ],
)
def test_pickling_no_error(method):
    pickled = pickle.dumps(method)
    unpickled = pickle.loads(pickled)
    assert unpickled
