"""Tests for the MFLOG line parser."""

from datetime import datetime, timezone

import pytest

from metaflow_extensions.logs.plugins.mflog_parser import (
    RUNTIME_SOURCE,
    TASK_SOURCE,
    MflogLine,
    parse_line,
    parse_stream,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_TS = "2024-03-15T10:30:45.123456"
_PATHSPEC = "MyFlow/123/train/456"


def _line(
    message: str = "hello world",
    source: str = TASK_SOURCE,
    ts: str = _TS,
    pathspec: str = _PATHSPEC,
    persist: bool = False,
) -> bytes:
    prefix = b"[!]" if persist else b""
    return prefix + f"[MFLOG|0|{ts}Z|{source}|{pathspec}]{message}".encode()


# ---------------------------------------------------------------------------
# parse_line — valid lines
# ---------------------------------------------------------------------------


class TestParseLineValid:
    def test_returns_mflog_line_for_valid_input(self):
        result = parse_line(_line())

        assert isinstance(result, MflogLine)

    def test_parses_message(self):
        result = parse_line(_line(message="training complete"))

        assert result is not None
        assert result.message == "training complete"

    def test_parses_task_source(self):
        result = parse_line(_line(source=TASK_SOURCE))

        assert result is not None
        assert result.source == TASK_SOURCE

    def test_parses_runtime_source(self):
        result = parse_line(_line(source=RUNTIME_SOURCE))

        assert result is not None
        assert result.source == RUNTIME_SOURCE

    def test_parses_pathspec_as_id(self):
        result = parse_line(_line(pathspec="MyFlow/42/process/7"))

        assert result is not None
        assert result.id == "MyFlow/42/process/7"

    def test_parses_timestamp_as_utc_datetime(self):
        result = parse_line(_line(ts="2024-03-15T10:30:45.123456"))

        assert result is not None
        expected = datetime(2024, 3, 15, 10, 30, 45, 123456, tzinfo=timezone.utc)
        assert result.timestamp == expected

    def test_timestamp_is_always_utc(self):
        result = parse_line(_line())

        assert result is not None
        assert result.timestamp.tzinfo == timezone.utc

    def test_should_persist_false_without_prefix(self):
        result = parse_line(_line(persist=False))

        assert result is not None
        assert result.should_persist is False

    def test_should_persist_true_with_bang_prefix(self):
        result = parse_line(_line(persist=True))

        assert result is not None
        assert result.should_persist is True

    def test_trailing_newline_is_stripped(self):
        result = parse_line(_line(message="hi") + b"\n")

        assert result is not None
        assert result.message == "hi"

    def test_message_with_brackets_preserved(self):
        result = parse_line(_line(message="[INFO] something [done]"))

        assert result is not None
        assert result.message == "[INFO] something [done]"

    def test_message_with_pipe_characters_preserved(self):
        result = parse_line(_line(message="a|b|c"))

        assert result is not None
        assert result.message == "a|b|c"


# ---------------------------------------------------------------------------
# parse_line — invalid / non-MFLOG lines
# ---------------------------------------------------------------------------


class TestParseLineInvalid:
    def test_returns_none_for_plain_text(self):
        assert parse_line(b"just a regular log line") is None

    def test_returns_none_for_empty_bytes(self):
        assert parse_line(b"") is None

    def test_returns_none_for_partial_mflog_header(self):
        assert parse_line(b"[MFLOG|0|incomplete") is None

    def test_returns_none_for_wrong_version(self):
        line = b"[MFLOG|1|2024-03-15T10:30:45Z|task|MyFlow/1/s/1]msg"
        assert parse_line(line) is None


# ---------------------------------------------------------------------------
# parse_stream
# ---------------------------------------------------------------------------


class TestParseStream:
    def test_parses_single_line(self):
        data = _line(message="step started") + b"\n"
        result = parse_stream(data)

        assert len(result) == 1
        assert result[0].message == "step started"

    def test_parses_multiple_lines_in_order(self):
        data = b"\n".join([_line(message="first"), _line(message="second")])
        result = parse_stream(data)

        assert [r.message for r in result] == ["first", "second"]

    def test_skips_non_mflog_lines(self):
        data = b"\n".join(
            [
                _line(message="valid"),
                b"raw output before metaflow bootstrapped",
                _line(message="also valid"),
            ]
        )
        result = parse_stream(data)

        assert len(result) == 2
        assert result[0].message == "valid"
        assert result[1].message == "also valid"

    def test_returns_empty_list_for_empty_input(self):
        assert parse_stream(b"") == []

    def test_returns_empty_list_when_no_mflog_lines(self):
        assert parse_stream(b"plain\ntext\nonly\n") == []

    @pytest.mark.parametrize("n", [1, 5, 100])
    def test_parses_n_lines(self, n: int):
        data = b"\n".join(_line(message=f"msg {i}") for i in range(n))
        result = parse_stream(data)

        assert len(result) == n
