import logging
import random
import shutil
import zlib
from collections import deque
from pathlib import Path

import pytest
from clarabot_nano_common.utility.path.common import normalize_absolute_path
from concurrent_log_handler import ConcurrentRotatingFileHandler

from clarabot_nano.utility.log_reader import ConcurrentRotatingFileReader
from clarabot_nano_tests.testing.file import temp_path


class _TestingConcurrentRotatingFileReader(ConcurrentRotatingFileReader):
    NEWLINE_ALLOWANCE = 1024
    RECORD_TEST_NUM = 1

    @classmethod
    def check_slice_logs(cls, logs: bytes, step: int):
        for nla in range(16):
            cls.NEWLINE_ALLOWANCE = nla
            cls._check_slice_logs_forward(logs, step)
            cls._check_slice_logs_backward(logs, step)

    @classmethod
    def _check_slice_logs_forward(cls, logs: bytes, step: int):
        for from_ in range(len(logs)):
            slices = []
            to = from_
            while True:
                slice_, to = cls.slice_logs_forward(logs, to, step)
                slices.append(slice_)
                if to == len(logs):
                    break
                assert slice_
            sliced = b"".join(slices)
            assert logs[from_:] == sliced, f"forward {step} {logs}\n{logs[from_:]}\n{sliced}"

    @classmethod
    def _check_slice_logs_backward(cls, logs: bytes, step: int):
        for from_ in range(len(logs), -1, -1):
            slices = []
            to = from_
            while True:
                slice_, to = cls.slice_logs_backward(logs, to, step)
                slices.append(slice_)
                if to == 0:
                    break
                assert slice_
            sliced = b"".join(reversed(slices))
            assert logs[:from_] == sliced, f"backward {step} {logs}\n{logs[:from_]}\n{sliced}"

    def __init__(self, handler: ConcurrentRotatingFileHandler, max_size: int):
        ConcurrentRotatingFileReader.__init__(self, handler, max_size)
        self.handler = handler
        self.read_buf = deque()

    def log_until_rollover(self, message_size: int, count: int = 1):
        n = self.handler.num_rollovers
        while (self.handler.num_rollovers - n) < count:
            self.do_log(message_size)

    def log_multiple(self, message_size: int, count: int = 1):
        for _ in range(count):
            self.do_log(message_size)

    def do_log(self, message_size: int):
        rn = _TestingConcurrentRotatingFileReader.RECORD_TEST_NUM
        msg = random.randbytes(max(1, message_size - 18))
        self.handler.emit(logging.LogRecord("_test", logging.ERROR, __file__, 100, "%08d %s %08d", (rn, msg, rn), None))
        _TestingConcurrentRotatingFileReader.RECORD_TEST_NUM = rn + 1

    def check_debug_logs(self, read_forward: bool = True, read_backward: bool = True, back_rotated_out: bool = False):
        current = self.get_debug_logs()
        streamed = self.current_buf()
        assert current and streamed
        if read_forward and read_backward:
            if back_rotated_out:
                assert streamed.endswith(current)
            else:
                assert streamed == current
        elif read_forward:
            assert current.endswith(streamed)
        elif back_rotated_out:
            int_head = streamed[-self.NEWLINE_ALLOWANCE :]
            i = current.find(int_head)
            assert i >= 0, str(int_head)
            assert streamed.endswith(current[: i + len(int_head)])
        else:
            assert current.find(streamed) >= 0

    def get_debug_logs(self) -> str:
        buf = []
        for ref in self._read_refs():
            if ref:
                buf.append(ref.read_bytes())
        return b"".join(reversed(buf)).decode("utf-8")

    def read(self, forward: bool = True) -> dict:
        first_read = self._state.is_new()
        data = ConcurrentRotatingFileReader.read(self, forward)
        if first_read:
            assert data.pop("new") is True
        if data.get("logs"):
            logs = data["logs"]
            if data.get("compression") == "deflate":
                logs = zlib.decompress(logs)
            if forward:
                self.read_buf.append(logs.decode("utf-8"))
            else:
                self.read_buf.appendleft(bytes(reversed(logs)).decode("utf-8"))
        else:
            logs = b""
        if not logs:
            assert data == ConcurrentRotatingFileReader.read(self, forward)
        return data

    def read_back_until_end(self):
        while True:
            data = self.read(False)
            if data.get("end"):
                break

    def read_back_until_head(self):
        while True:
            data = self.read()
            if not data.get("logs"):
                break

    def current_buf(self) -> str:
        return "".join(self.read_buf)


def test_log_reader_slice_logs_forward():
    _TestingConcurrentRotatingFileReader.NEWLINE_ALLOWANCE = 5
    step = 10
    for i, f, o, t in [
        (b"", 0, b"", 0),
        (b"0123456789\nabcdefghij\n", 0, b"0123456789", 10),
        (b"01234\n56789\nabcde\nfghij\n", 0, b"01234\n", 6),
        (b"0123456\x80\x80\x80\x80\nabcde\n", 0, b"012345", 6),
        (b"01234567\x80\x80\x80\nabcde\n", 0, b"0123456", 7),
        (b"012345\x80\x80\x80\x80\x80\nabcde\n", 0, b"012345\x80\x80\x80\x80", 10),
    ]:
        assert _TestingConcurrentRotatingFileReader.slice_logs_forward(i, f, step) == (o, t), f"{i} {f}"


def test_log_reader_slice_logs_backward():
    _TestingConcurrentRotatingFileReader.NEWLINE_ALLOWANCE = 5
    step = 10
    for i, f, o, t in [
        (b"", 0, b"", 0),
        (b"0123456789\nabcdefghij\n", 22, b"bcdefghij\n", 12),
        (b"01234\n56789\nabcde\nfghij\n", 24, b"fghij\n", 18),
        (b"0123456\n\x80\x80\x80\x80\x80abcd\n", 18, b"\x80\x80\x80\x80\x80abcd\n", 8),
        (b"0123456\n\x80\x80\x80\x80abcde\n", 18, b"\x80\x80\x80\x80abcde\n", 8),
        (b"0123456\x80\x80\x80\x80\x80abcde\n", 18, b"abcde\n", 12),
    ]:
        assert _TestingConcurrentRotatingFileReader.slice_logs_backward(i, f, step) == (o, t), f"{i} {f}"


def test_log_reader_slice_logs_patterns():
    az = bytes(range(97, 123))
    abc = b"abc"
    c4 = bytes([0x80] * 4)
    nl = b"\n"
    logs_patterns = [
        b"",
        az,
        (c4 * 6),
        nl * 20,
        b"a" + (c4 * 5) + b"z",
        nl.join([abc * 2] * 3),
        nl.join([c4 * 2] * 3),
        (nl * 8).join([abc * 2] * 3),
        (nl * 8).join([c4 * 2] * 3),
    ]

    for step in (8, 9, 16, 1024):
        for logs in logs_patterns:
            _TestingConcurrentRotatingFileReader.check_slice_logs(logs, step)


@pytest.fixture
def log_file(tmpdir):
    d = Path(normalize_absolute_path(temp_path(tmpdir)))
    d.mkdir(parents=True)
    f = d / "some-log.log"
    return f


@pytest.mark.parametrize("use_gzip", [False, True])
def test_log_reader_simulate(log_file, use_gzip):
    _TestingConcurrentRotatingFileReader.NEWLINE_ALLOWANCE = 1024
    lh = ConcurrentRotatingFileHandler(str(log_file), maxBytes=6000, backupCount=3, use_gzip=use_gzip)

    lr2 = _TestingConcurrentRotatingFileReader(lh, 120000)
    for _ in range(2):
        assert lr2.read() == {"logs": b""}

    lr = _TestingConcurrentRotatingFileReader(lh, 120000)
    lr.log_until_rollover(500)
    lr.read()
    lr.check_debug_logs(read_backward=False)
    lr.read_back_until_end()
    lr.check_debug_logs()
    lr.log_multiple(300, 3)
    lr.check_debug_logs(read_forward=False)
    for _ in range(2):
        lr.read()
        lr.check_debug_logs()
    lr.log_until_rollover(500, count=2)
    lr.check_debug_logs(read_forward=False)
    lr.read()
    lr.check_debug_logs(read_forward=False)
    lr.read()
    lr.check_debug_logs(read_forward=False)
    for _ in range(2):
        lr.read()
        lr.check_debug_logs()

    lr.log_until_rollover(500, count=1)
    lr.check_debug_logs(read_forward=False, back_rotated_out=True)
    lr.read()
    lr.check_debug_logs(read_forward=False, back_rotated_out=True)
    for _ in range(2):
        lr.read()
        lr.check_debug_logs(back_rotated_out=True)

    # test missing head (interrupted during rollover)
    lr.log_until_rollover(500, count=1)
    shutil.move(lh.baseFilename, log_file.parent / "interr.log")
    lr.check_debug_logs(read_forward=False, back_rotated_out=True)
    for _ in range(2):
        lr.read()
        lr.check_debug_logs(back_rotated_out=True)
    # check beginning with archive
    lr2 = _TestingConcurrentRotatingFileReader(lh, 120000)
    lr2.read()
    lr2.check_debug_logs(read_backward=False)
    shutil.move(log_file.parent / "interr.log", lh.baseFilename)
    lr.check_debug_logs(read_forward=False, back_rotated_out=True)
    lr.read()
    lr.check_debug_logs(back_rotated_out=True)

    # test rollover beyond tracking
    lr.log_until_rollover(500, count=3)
    for _ in range(2):
        assert lr.read().get("reset")


@pytest.mark.parametrize("max_bytes", [50000, 1000000])
@pytest.mark.parametrize("window_size", [16000, 400000])
@pytest.mark.parametrize("use_gzip", [False, True])
def test_log_reader_simulate2(log_file, use_gzip, window_size, max_bytes):
    _TestingConcurrentRotatingFileReader.NEWLINE_ALLOWANCE = 1024
    lh = ConcurrentRotatingFileHandler(str(log_file), maxBytes=max_bytes, backupCount=3, use_gzip=use_gzip)

    lr = _TestingConcurrentRotatingFileReader(lh, window_size)
    lr.log_until_rollover(800)
    lr.read()
    lr.check_debug_logs(read_backward=False)
    lr.log_until_rollover(800)
    lr.check_debug_logs(read_forward=False, read_backward=False)
    lr.read()
    lr.check_debug_logs(read_forward=False, read_backward=False)
    lr.read_back_until_head()
    lr.check_debug_logs(read_backward=False)

    lr.read_back_until_end()
    lr.check_debug_logs()
