"""Tests for centralized logging configuration module."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch

import pytest
import structlog

from noteflow.infrastructure.logging.config import (
    LoggingConfig,
    configure_logging,
    create_renderer,
    get_log_level,
    get_logger,
)

if TYPE_CHECKING:
    from collections.abc import Generator
    from typing import TextIO


# Test constants
DEFAULT_LEVEL = "INFO"
DEBUG_LEVEL = "DEBUG"
WARNING_LEVEL = "WARNING"
ERROR_LEVEL = "ERROR"
CRITICAL_LEVEL = "CRITICAL"
INVALID_LEVEL = "INVALID"


class TestLoggingConfig:
    """Tests for LoggingConfig dataclass."""

    def test_config_has_default_values(self) -> None:
        """LoggingConfig initializes with sensible defaults."""
        config = LoggingConfig()
        assert config.level == DEFAULT_LEVEL, "default level should be INFO"
        assert config.json_file is None, "json_file should default to None"
        assert config.enable_console is True, "console should be enabled by default"
        assert config.log_format == "auto", "log_format should default to 'auto'"
        assert config.enable_log_buffer is True, "log buffer should be enabled by default"
        assert config.enable_otel_context is True, "OTEL context should be enabled by default"
        assert config.enable_noteflow_context is True, "NoteFlow context should be enabled"
        assert config.console_colors is True, "console colors should be enabled by default"

    def test_config_accepts_custom_level(self) -> None:
        """LoggingConfig accepts custom log level."""
        config = LoggingConfig(level=DEBUG_LEVEL)
        assert config.level == DEBUG_LEVEL, "level should be DEBUG when specified"

    def test_config_accepts_json_file_path(self, tmp_path: Path) -> None:
        """LoggingConfig accepts JSON file path."""
        expected_path = tmp_path / "noteflow.json"
        config = LoggingConfig(json_file=expected_path)
        assert config.json_file == expected_path, "json_file should match provided path"

    def test_config_is_frozen(self) -> None:
        """LoggingConfig is immutable (frozen dataclass)."""
        config = LoggingConfig()
        with pytest.raises(Exception) as excinfo:
            config.level = DEBUG_LEVEL
        assert isinstance(excinfo.value, (AttributeError, TypeError))


class TestGetLogLevel:
    """Tests for get_log_level helper function."""

    @pytest.mark.parametrize(
        ("level_name", "expected"),
        [
            (DEBUG_LEVEL, logging.DEBUG),
            (DEFAULT_LEVEL, logging.INFO),
            (WARNING_LEVEL, logging.WARNING),
            (ERROR_LEVEL, logging.ERROR),
            (CRITICAL_LEVEL, logging.CRITICAL),
        ],
    )
    def test_converts_valid_level_names(self, level_name: str, expected: int) -> None:
        """get_log_level converts valid level names to constants."""
        result = get_log_level(level_name)
        assert result == expected, f"{level_name} should map to {expected}"

    def test_handles_lowercase_level_names(self) -> None:
        """get_log_level handles lowercase level names."""
        result = get_log_level("debug")
        assert result == logging.DEBUG, "lowercase 'debug' should map to DEBUG"

    def test_returns_info_for_invalid_level(self) -> None:
        """get_log_level returns INFO for invalid level names."""
        result = get_log_level(INVALID_LEVEL)
        assert result == logging.INFO, "invalid level should default to INFO"


class TestCreateRenderer:
    """Tests for create_renderer helper function."""

    def test_returns_json_renderer_when_json_format_enabled(self) -> None:
        """create_renderer returns JSONRenderer when log_format is 'json'."""
        config = LoggingConfig(log_format="json")
        renderer = create_renderer(config)
        assert isinstance(
            renderer, structlog.processors.JSONRenderer
        ), "should return JSONRenderer when JSON console enabled"

    def test_returns_json_renderer_when_not_tty(self) -> None:
        """create_renderer returns JSONRenderer when stderr is not a TTY."""
        config = LoggingConfig(log_format="auto")
        with patch("sys.stderr") as mock_stderr:
            mock_stderr.isatty.return_value = False
            renderer = create_renderer(config)
        assert isinstance(
            renderer, structlog.processors.JSONRenderer
        ), "should return JSONRenderer when not a TTY"

    def test_returns_console_renderer_for_tty(self) -> None:
        """create_renderer returns ConsoleRenderer for TTY with colors."""
        config = LoggingConfig(log_format="auto", console_colors=True)
        with patch("sys.stderr") as mock_stderr:
            mock_stderr.isatty.return_value = True
            renderer = create_renderer(config)
        assert isinstance(
            renderer, structlog.dev.ConsoleRenderer
        ), "should return ConsoleRenderer for TTY"


@pytest.fixture
def clean_root_logger() -> Generator[None, None, None]:
    """Clean up root logger handlers after test."""
    root = logging.getLogger()
    original_handlers = root.handlers[:]
    original_level = root.level
    yield
    # Restore original state
    root.handlers = original_handlers
    root.setLevel(original_level)


@pytest.mark.usefixtures("clean_root_logger")
class TestConfigureLogging:
    """Tests for configure_logging function."""

    def test_configures_with_default_config(self) -> None:
        """configure_logging works with default config."""
        configure_logging()
        root = logging.getLogger()
        assert root.level == logging.INFO, "root logger should be set to INFO"

    def test_configures_with_custom_level(self) -> None:
        """configure_logging accepts custom level parameter."""
        configure_logging(level=DEBUG_LEVEL)
        root = logging.getLogger()
        assert root.level == logging.DEBUG, "root logger should be set to DEBUG"

    def test_configures_with_config_object(self) -> None:
        """configure_logging accepts LoggingConfig object."""
        config = LoggingConfig(level=WARNING_LEVEL, enable_console=False)
        configure_logging(config)
        root = logging.getLogger()
        assert root.level == logging.WARNING, "root logger should be set to WARNING"

    def test_clears_existing_handlers(self) -> None:
        """configure_logging clears existing handlers before adding new ones."""
        root = logging.getLogger()
        dummy_handler = logging.StreamHandler()
        root.addHandler(dummy_handler)

        configure_logging(config=LoggingConfig(enable_console=True, enable_log_buffer=False))

        # Should have cleared old handlers and added console handler
        assert (
            dummy_handler not in root.handlers
        ), "existing handlers should be cleared"

    def test_adds_console_handler_when_enabled(self) -> None:
        """configure_logging adds console handler when enabled."""
        configure_logging(config=LoggingConfig(enable_console=True, enable_log_buffer=False))
        root = logging.getLogger()
        stream_handlers: list[logging.StreamHandler[TextIO]] = [
            h for h in root.handlers if isinstance(h, logging.StreamHandler)
        ]
        assert stream_handlers, "should add StreamHandler for console output"


@pytest.mark.usefixtures("clean_root_logger")
class TestGetLogger:
    """Tests for get_logger function."""

    def test_returns_bound_logger(self) -> None:
        """get_logger returns a structlog logger with standard methods."""
        configure_logging()
        logger = get_logger("test.module")
        # structlog returns BoundLoggerLazyProxy which wraps BoundLogger
        assert hasattr(logger, "info"), "logger should have info method"
        assert hasattr(logger, "debug"), "logger should have debug method"
        assert hasattr(logger, "error"), "logger should have error method"

    def test_returns_logger_without_name(self) -> None:
        """get_logger works without name argument."""
        configure_logging()
        logger = get_logger()
        assert logger is not None, "should return logger without explicit name"
