#!/usr/bin/env python

# Copyright (C) 2018 Visual Collaboration Technologies Inc.
# All Rights Reserved.
#
# This file is a property of Visual Collaboration Technologies Inc.
# Unauthorized access, reproduction or redistribution of any kind is prohibited.
"""Tests for logging setup and configuration."""

import logging
import re
from unittest.mock import patch

import pytest

import vcti.logging
from vcti.logging import create_logging_config, logger, setup_logging
from vcti.logging.config import (
    MESSAGE_FORMAT_PRESETS,
    ConsoleLogConfig,
    FileLogConfig,
    resolve_message_format,
)


class TestVersion:
    def test_version_exists(self):
        assert hasattr(vcti.logging, "__version__")

    def test_version_is_valid_semver(self):
        assert re.match(r"^\d+\.\d+\.\d+", vcti.logging.__version__)


@pytest.fixture
def log_file(tmp_path):
    """Fixture providing a temporary log file path."""
    return tmp_path / "test.log"


@pytest.fixture
def log_config(log_file):
    """Fixture providing logging configuration."""
    return create_logging_config(enable_console_log=True, log_file_path=log_file)


@pytest.fixture(autouse=True)
def setup_teardown_logging(log_config):
    """Setup and teardown logging for each test."""
    # Setup
    setup_logging(log_config)
    yield
    # Teardown
    for handler in logger.handlers[:]:
        handler.close()
        logger.removeHandler(handler)


class TestLoggingSetup:
    """Tests for setup_logging functionality."""

    def test_setup_logging(self):
        """Ensure logging is set up correctly."""
        assert logger.hasHandlers()

    def test_log_file_created(self, log_file):
        """Test that log file is created when logging."""
        test_message = "Test log message"
        logger.info(test_message)

        assert log_file.exists()
        assert test_message in log_file.read_text()

    def test_invalid_log_level(self):
        """Ensure invalid log levels raise ValueError."""
        with pytest.raises(ValueError):
            setup_logging(create_logging_config(log_level="invalid"))  # type: ignore

    def test_logging_levels(self, log_file):
        """Test different logging levels."""
        test_messages = {
            "info": "Info message",
            "warning": "Warning message",
            "error": "Error message",
            "fatal": "Fatal message",
        }

        for level, message in test_messages.items():
            getattr(logger, level)(message)

        log_content = log_file.read_text()
        for message in test_messages.values():
            assert message in log_content

    def test_logging_without_file(self):
        """Test logging setup without file logging."""
        config = create_logging_config(enable_console_log=True, log_file_path=None)
        setup_logging(config)

        assert logger.hasHandlers()
        assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers)
        assert not any(isinstance(h, logging.FileHandler) for h in logger.handlers)

    def test_logging_with_file_only(self, tmp_path):
        """Test logging setup with file logging only (no console)."""
        log_file = tmp_path / "file_only.log"
        config = create_logging_config(enable_console_log=False, log_file_path=log_file)
        setup_logging(config)

        assert logger.hasHandlers()
        assert any(isinstance(h, logging.FileHandler) for h in logger.handlers)
        # FileHandler is a subclass of StreamHandler, so check for console handlers specifically
        # by excluding FileHandler instances
        assert not any(
            isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
            for h in logger.handlers
        )

    def test_multiple_handlers(self, tmp_path):
        """Test that both console and file handlers are created."""
        log_file = tmp_path / "multi.log"
        config = create_logging_config(enable_console_log=True, log_file_path=log_file)
        setup_logging(config)

        assert len(logger.handlers) == 2
        assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers)
        assert any(isinstance(h, logging.FileHandler) for h in logger.handlers)

    def test_logger_level_set_correctly(self, tmp_path):
        """Test that logger level is set to minimum of handler levels."""
        log_file = tmp_path / "level.log"
        config = create_logging_config(
            enable_console_log=True, log_file_path=log_file, log_level="debug"
        )
        setup_logging(config)

        # Logger level should be set to the minimum level of all handlers
        assert logger.level == logging.DEBUG

    def test_propagate_flag(self, log_file):
        """Test that propagate flag can be configured."""
        config = create_logging_config(enable_console_log=True, log_file_path=log_file)

        # Test with propagate=False (default)
        setup_logging(config, propagate=False)
        assert logger.propagate is False

        # Test with propagate=True
        setup_logging(config, propagate=True)
        assert logger.propagate is True

    def test_reconfigure_replaces_handlers(self, tmp_path):
        """Test that calling setup_logging twice replaces handlers, not appends."""
        log_file_1 = tmp_path / "first.log"
        log_file_2 = tmp_path / "second.log"

        config_1 = create_logging_config(enable_console_log=True, log_file_path=log_file_1)
        setup_logging(config_1)
        handler_count_after_first = len(logger.handlers)

        config_2 = create_logging_config(enable_console_log=True, log_file_path=log_file_2)
        setup_logging(config_2)
        handler_count_after_second = len(logger.handlers)

        assert handler_count_after_first == handler_count_after_second

    def test_file_handler_uses_plain_formatter(self, tmp_path):
        """Test that file handler uses plain Formatter, not ColoredFormatter."""
        log_file = tmp_path / "plain.log"
        config = create_logging_config(
            enable_console_log=True,
            enable_color_log=True,
            log_file_path=log_file,
            log_level="info",
        )
        setup_logging(config)

        file_handlers = [h for h in logger.handlers if isinstance(h, logging.FileHandler)]
        assert len(file_handlers) == 1
        formatter = file_handlers[0].formatter
        assert type(formatter) is logging.Formatter

    def test_file_log_has_no_ansi_codes(self, tmp_path):
        """Test that file log output contains no ANSI escape codes."""
        log_file = tmp_path / "no_ansi.log"
        config = create_logging_config(
            enable_console_log=False,
            log_file_path=log_file,
            log_level="info",
        )
        setup_logging(config)

        logger.info("Test message")
        logger.warning("Warning message")
        logger.error("Error message")

        log_content = log_file.read_text()
        assert "\033[" not in log_content  # ANSI escape sequence prefix

    def test_gui_safety_no_streams(self):
        """Test that console handler is skipped when stdout and stderr are None."""
        config = create_logging_config(enable_console_log=True, log_file_path=None)

        with patch("vcti.logging.logger_setup.sys") as mock_sys:
            mock_sys.stderr = None
            mock_sys.stdout = None
            setup_logging(config)

        # No handlers should be added since console was the only config
        # and both streams are None
        console_handlers = [
            h
            for h in logger.handlers
            if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
        ]
        assert len(console_handlers) == 0

    def test_gui_safety_falls_back_to_stdout(self, tmp_path):
        """Test that console handler uses stdout when stderr is None."""
        config = create_logging_config(enable_console_log=True, log_file_path=None)

        with patch("vcti.logging.logger_setup.sys") as mock_sys:
            mock_sys.stderr = None
            mock_sys.stdout = open(tmp_path / "stdout.log", "w")
            try:
                setup_logging(config)

                console_handlers = [
                    h
                    for h in logger.handlers
                    if isinstance(h, logging.StreamHandler)
                    and not isinstance(h, logging.FileHandler)
                ]
                assert len(console_handlers) == 1
                assert console_handlers[0].stream is mock_sys.stdout
            finally:
                mock_sys.stdout.close()


class TestLoggingConfig:
    """Tests for create_logging_config functionality."""

    def test_logging_config_defaults(self):
        """Test default logging configuration."""
        config = create_logging_config()
        assert len(config) == 1
        assert isinstance(config[0], ConsoleLogConfig)
        assert config[0].log_level == "info"
        assert config[0].enable_color_log is False

    def test_logging_config_with_file(self, tmp_path):
        """Test logging configuration with both console and file logging."""
        log_file = tmp_path / "test.log"
        config = create_logging_config(
            enable_console_log=True, log_file_path=log_file, log_level="debug"
        )
        assert len(config) == 2
        assert isinstance(config[0], ConsoleLogConfig)
        assert isinstance(config[1], FileLogConfig)
        assert config[0].log_level == "debug"
        assert config[1].log_level == "debug"
        assert config[1].file_path == log_file

    def test_logging_config_console_only(self):
        """Test logging configuration with console logging only."""
        config = create_logging_config(enable_console_log=True, log_file_path=None)
        assert len(config) == 1
        assert isinstance(config[0], ConsoleLogConfig)

    def test_logging_config_file_only(self, tmp_path):
        """Test logging configuration with file logging only."""
        log_file = tmp_path / "test.log"
        config = create_logging_config(enable_console_log=False, log_file_path=log_file)
        assert len(config) == 1
        assert isinstance(config[0], FileLogConfig)
        assert config[0].file_path == log_file

    def test_logging_config_no_handlers(self):
        """Test that empty config is created when no handlers requested."""
        config = create_logging_config(enable_console_log=False, log_file_path=None)
        assert len(config) == 0

    def test_logging_config_with_color(self):
        """Test logging configuration with color enabled."""
        config = create_logging_config(enable_console_log=True, enable_color_log=True)
        assert len(config) == 1
        assert isinstance(config[0], ConsoleLogConfig)
        assert config[0].enable_color_log is True

    def test_logging_config_custom_log_level(self):
        """Test logging configuration with custom log level."""
        config = create_logging_config(log_level="trace")
        assert config[0].log_level == "trace"

    def test_logging_config_fatal_level(self):
        """Test logging configuration with fatal log level."""
        config = create_logging_config(log_level="fatal")
        assert config[0].log_level == "fatal"

    def test_logging_config_custom_message_format(self):
        """Test logging configuration with custom message format."""
        custom_format = "%(levelname)s - %(message)s"
        config = create_logging_config(message_format=custom_format)
        assert config[0].message_format == custom_format

    def test_logging_config_all_options(self, tmp_path):
        """Test logging configuration with all options specified."""
        log_file = tmp_path / "test.log"
        custom_format = "%(levelname)s: %(message)s"
        config = create_logging_config(
            enable_console_log=True,
            enable_color_log=True,
            log_file_path=log_file,
            log_level="verbose",
            message_format=custom_format,
        )
        assert len(config) == 2
        assert config[0].log_level == "verbose"
        assert config[1].log_level == "verbose"
        assert isinstance(config[0], ConsoleLogConfig)
        assert config[0].enable_color_log is True
        # Note: Color format is different for console vs file
        assert config[0].message_format == custom_format
        assert config[1].message_format == custom_format


class TestMessageFormatPresets:
    """Tests for message format preset resolution."""

    def test_resolve_preset_plain(self):
        """Test resolving a preset without color."""
        for name, (plain, _colored) in MESSAGE_FORMAT_PRESETS.items():
            assert resolve_message_format(name, color=False) == plain

    def test_resolve_preset_color(self):
        """Test resolving a preset with color."""
        for name, (_plain, colored) in MESSAGE_FORMAT_PRESETS.items():
            assert resolve_message_format(name, color=True) == colored

    def test_resolve_none_uses_default(self):
        """Test that None resolves to the default preset."""
        result = resolve_message_format(None, color=False)
        assert result == MESSAGE_FORMAT_PRESETS["standard"][0]

    def test_resolve_raw_format_passthrough(self):
        """Test that a raw format string passes through unchanged."""
        raw = "%(levelname)s - %(message)s"
        assert resolve_message_format(raw, color=False) == raw
        assert resolve_message_format(raw, color=True) == raw

    def test_preset_used_in_console_handler(self, tmp_path):
        """Test that preset names are resolved when creating console handlers."""
        config = [ConsoleLogConfig(log_level="info", message_format="detailed")]
        setup_logging(config)

        handler = logger.handlers[0]
        expected = MESSAGE_FORMAT_PRESETS["detailed"][0]
        assert handler.formatter._fmt == expected

    def test_preset_used_in_file_handler(self, tmp_path):
        """Test that preset names are resolved when creating file handlers."""
        log_file = tmp_path / "preset.log"
        config = [FileLogConfig(file_path=log_file, log_level="info", message_format="minimal")]
        setup_logging(config)

        file_handler = [h for h in logger.handlers if isinstance(h, logging.FileHandler)][0]
        expected = MESSAGE_FORMAT_PRESETS["minimal"][0]
        assert file_handler.formatter._fmt == expected

    def test_color_preset_resolved_for_color_console(self):
        """Test that color variant is used when enable_color_log is True."""
        config = [
            ConsoleLogConfig(log_level="info", message_format="standard", enable_color_log=True)
        ]
        setup_logging(config)

        handler = logger.handlers[0]
        expected = MESSAGE_FORMAT_PRESETS["standard"][1]
        assert handler.formatter._fmt == expected

    def test_different_presets_per_handler(self, tmp_path):
        """Test that console and file can use different presets."""
        log_file = tmp_path / "diff_preset.log"
        config = [
            ConsoleLogConfig(log_level="info", message_format="detailed"),
            FileLogConfig(file_path=log_file, log_level="info", message_format="minimal"),
        ]
        setup_logging(config)

        console_handler = [
            h
            for h in logger.handlers
            if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
        ][0]
        file_handler = [h for h in logger.handlers if isinstance(h, logging.FileHandler)][0]

        assert console_handler.formatter._fmt == MESSAGE_FORMAT_PRESETS["detailed"][0]
        assert file_handler.formatter._fmt == MESSAGE_FORMAT_PRESETS["minimal"][0]


class TestFileHandlerMode:
    """Tests for configurable file handler mode."""

    def test_default_mode_is_append(self, tmp_path):
        """Test that the default file mode is append."""
        log_file = tmp_path / "append.log"
        config = FileLogConfig(file_path=log_file, log_level="info")
        assert config.mode == "a"

    def test_append_mode_preserves_content(self, tmp_path):
        """Test that append mode preserves existing log content."""
        log_file = tmp_path / "append.log"

        # First setup and log
        config = [FileLogConfig(file_path=log_file, log_level="info", message_format="minimal")]
        setup_logging(config)
        logger.info("first message")

        # Second setup (simulating restart) and log
        setup_logging(config)
        logger.info("second message")

        content = log_file.read_text()
        assert "first message" in content
        assert "second message" in content

    def test_write_mode_overwrites_content(self, tmp_path):
        """Test that write mode overwrites existing log content."""
        log_file = tmp_path / "overwrite.log"

        # First setup and log
        config = [
            FileLogConfig(file_path=log_file, log_level="info", message_format="minimal", mode="w")
        ]
        setup_logging(config)
        logger.info("first message")

        # Second setup (simulating restart) and log
        setup_logging(config)
        logger.info("second message")

        content = log_file.read_text()
        assert "first message" not in content
        assert "second message" in content
