from __future__ import annotations

import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from textwrap import dedent
from unittest.mock import MagicMock, patch

import pytest

from vibe.core.logger import (
    StructuredLogFormatter,
    apply_logging_config,
    decode_log_message,
    encode_log_message,
)


@pytest.fixture
def mock_log_dir(tmp_path: Path):
    """Mock LOG_DIR and LOG_FILE to use tmp_path for testing."""
    mock_dir = MagicMock()
    mock_dir.path = tmp_path
    mock_file = MagicMock()
    mock_file.path = tmp_path / "vibe.log"
    with (
        patch("vibe.core.logger.LOG_DIR", mock_dir),
        patch("vibe.core.logger.LOG_FILE", mock_file),
    ):
        yield tmp_path


class TestStructuredFormatter:
    def test_format_contains_required_fields(self) -> None:
        formatter = StructuredLogFormatter()
        record = logging.LogRecord(
            name="test_logger",
            level=logging.INFO,
            pathname="test.py",
            lineno=1,
            msg="Test message",
            args=(),
            exc_info=None,
        )

        output = formatter.format(record)

        parts = output.split(" ", 4)
        assert len(parts) == 5
        assert "T" in parts[0]
        assert parts[1].isdigit()
        assert parts[2].isdigit()
        assert parts[3] == "INFO"
        assert parts[4] == "Test message"

    def test_format_includes_exception(self) -> None:
        formatter = StructuredLogFormatter()
        try:
            raise ValueError("test error")
        except ValueError:
            import sys

            exc_info = sys.exc_info()

        record = logging.LogRecord(
            name="test_logger",
            level=logging.ERROR,
            pathname="test.py",
            lineno=1,
            msg="Error occurred",
            args=(),
            exc_info=exc_info,
        )

        output = formatter.format(record)

        assert "Error occurred" in output
        assert "ValueError" in output
        assert "test error" in output

    def test_format_escapes_newlines_in_message(self) -> None:
        formatter = StructuredLogFormatter()
        multiline_msg = dedent("""
            Line one
            Line two
            Line three""")
        record = logging.LogRecord(
            name="test_logger",
            level=logging.INFO,
            pathname="test.py",
            lineno=1,
            msg=multiline_msg,
            args=(),
            exc_info=None,
        )

        output = formatter.format(record)

        assert "\n" not in output
        assert "Line one\\nLine two\\nLine three" in output

    def test_format_escapes_newlines_in_exception(self) -> None:
        formatter = StructuredLogFormatter()
        try:
            raise ValueError("test error")
        except ValueError:
            import sys

            exc_info = sys.exc_info()

        record = logging.LogRecord(
            name="test_logger",
            level=logging.ERROR,
            pathname="test.py",
            lineno=1,
            msg="Error occurred",
            args=(),
            exc_info=exc_info,
        )

        output = formatter.format(record)

        assert "\n" not in output
        assert "ValueError" in output
        assert "test error" in output
        assert "\\n" in output

    def test_format_output_is_single_line(self) -> None:
        formatter = StructuredLogFormatter()
        try:
            error_msg = dedent("""
                multi
                line
                error""")
            raise RuntimeError(error_msg)
        except RuntimeError:
            import sys

            exc_info = sys.exc_info()

        multiline_msg = dedent("""
            Something
            went
            wrong""")
        record = logging.LogRecord(
            name="test_logger",
            level=logging.ERROR,
            pathname="test.py",
            lineno=1,
            msg=multiline_msg,
            args=(),
            exc_info=exc_info,
        )

        output = formatter.format(record)
        lines = output.split("\n")

        assert len(lines) == 1


class TestApplyLoggingConfig:
    def test_adds_handler_to_logger(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_LEVEL", "DEBUG")
        test_logger = logging.getLogger("test_apply_logging")
        initial_handler_count = len(test_logger.handlers)

        apply_logging_config(test_logger)

        assert len(test_logger.handlers) == initial_handler_count + 1

    def test_creates_log_file(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_LEVEL", "DEBUG")
        test_logger = logging.getLogger("test_log_file")
        test_logger.setLevel(logging.DEBUG)

        apply_logging_config(test_logger)
        test_logger.info("Test log entry")

        log_file = mock_log_dir / "vibe.log"
        assert log_file.exists()

    def test_log_entry_format(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_LEVEL", "DEBUG")
        test_logger = logging.getLogger("test_format")
        test_logger.setLevel(logging.DEBUG)

        apply_logging_config(test_logger)
        test_logger.warning("Test warning message")

        log_file = mock_log_dir / "vibe.log"
        content = log_file.read_text()

        assert "WARNING" in content
        assert "Test warning message" in content

    def test_respects_log_level(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_LEVEL", "WARNING")
        test_logger = logging.getLogger("test_level_filter")
        test_logger.setLevel(logging.DEBUG)

        apply_logging_config(test_logger)
        test_logger.debug("Debug message")
        test_logger.info("Info message")
        test_logger.warning("Warning message")

        log_file = mock_log_dir / "vibe.log"
        content = log_file.read_text()

        assert "Debug message" not in content
        assert "Info message" not in content
        assert "Warning message" in content

    def test_creates_log_directory_if_missing(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        log_dir = tmp_path / "nested" / "logs"
        mock_dir = MagicMock()
        mock_dir.path = log_dir
        mock_file = MagicMock()
        mock_file.path = log_dir / "vibe.log"
        with (
            patch("vibe.core.logger.LOG_DIR", mock_dir),
            patch("vibe.core.logger.LOG_FILE", mock_file),
        ):
            monkeypatch.setenv("LOG_LEVEL", "DEBUG")
            test_logger = logging.getLogger("test_mkdir")

            apply_logging_config(test_logger)

            assert log_dir.exists()

    def test_debug_mode_overrides_log_level(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_LEVEL", "WARNING")
        monkeypatch.setenv("DEBUG_MODE", "true")
        test_logger = logging.getLogger("test_debug_mode")
        test_logger.setLevel(logging.DEBUG)

        apply_logging_config(test_logger)
        test_logger.debug("Debug message")

        log_file = mock_log_dir / "vibe.log"
        content = log_file.read_text()

        assert "Debug message" in content

    def test_invalid_log_level_defaults_to_warning(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_LEVEL", "INVALID")
        test_logger = logging.getLogger("test_invalid_level")
        test_logger.setLevel(logging.DEBUG)

        apply_logging_config(test_logger)
        test_logger.info("Info message")
        test_logger.warning("Warning message")

        log_file = mock_log_dir / "vibe.log"
        content = log_file.read_text()

        assert "Info message" not in content
        assert "Warning message" in content

    def test_log_max_bytes_from_env(
        self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("LOG_MAX_BYTES", "5242880")  # 5 MB
        monkeypatch.setenv("LOG_LEVEL", "DEBUG")
        test_logger = logging.getLogger("test_max_bytes")

        apply_logging_config(test_logger)

        # Verify handler was added with correct maxBytes
        handler = test_logger.handlers[-1]
        assert isinstance(handler, RotatingFileHandler)
        assert handler.maxBytes == 5242880


class TestDecodeLogMessage:
    def test_plain_message_unchanged(self) -> None:
        assert decode_log_message("Hello world") == "Hello world"

    def test_decodes_escaped_newline(self) -> None:
        assert decode_log_message("hello\\nworld") == "hello\nworld"

    def test_decodes_escaped_backslash(self) -> None:
        assert decode_log_message("C:\\\\path") == "C:\\path"

    def test_decodes_escaped_backslash_before_n(self) -> None:
        # This is the bug case: C:\new encoded as C:\\new must decode back to C:\new
        assert decode_log_message("C:\\\\new") == "C:\\new"

    def test_roundtrip_with_newlines(self) -> None:
        original = "line one\nline two\nline three"
        encoded = encode_log_message(original)
        assert decode_log_message(encoded) == original

    def test_roundtrip_with_backslashes(self) -> None:
        original = "C:\\Users\\test\\file.txt"
        encoded = encode_log_message(original)
        assert decode_log_message(encoded) == original

    def test_roundtrip_with_backslash_n(self) -> None:
        original = "C:\\new folder\\notes.txt"
        encoded = encode_log_message(original)
        assert decode_log_message(encoded) == original

    def test_roundtrip_mixed(self) -> None:
        original = "path: C:\\new\nand a newline"
        encoded = encode_log_message(original)
        assert decode_log_message(encoded) == original

    def test_exception_encoding_escapes_backslashes(self) -> None:
        formatter = StructuredLogFormatter()
        try:
            raise ValueError("error in C:\\new\\path")
        except ValueError:
            import sys

            exc_info = sys.exc_info()

        record = logging.LogRecord(
            name="test",
            level=logging.ERROR,
            pathname="test.py",
            lineno=1,
            msg="fail",
            args=(),
            exc_info=exc_info,
        )

        output = formatter.format(record)

        assert "\n" not in output
        # The backslashes in the exception should be escaped
        assert "C:\\\\new\\\\path" in output
