"""
Tests for structured JSON logging functionality.

Tests cover:
- JSONFormatter formatting
- StructuredLogger functionality
- Log parsing and validation
- Exception handling
- Context field injection
"""

import pytest
import logging
import json
import io

from atomingest.observability.json_logging import (
    JSONFormatter,
    StructuredLogger,
    configure_structured_logging,
    get_logger,
)


class TestJSONFormatter:
    """Tests for JSONFormatter class."""

    @pytest.fixture
    def formatter(self):
        """Create a JSONFormatter instance."""
        return JSONFormatter()

    @pytest.fixture
    def log_stream(self):
        """Create a string stream for capturing log output."""
        return io.StringIO()

    @pytest.fixture
    def logger_with_formatter(self, log_stream, formatter):
        """Create a logger configured with JSON formatter."""
        logger = logging.getLogger("test_json_logger")
        logger.setLevel(logging.DEBUG)
        logger.handlers = []  # Clear any existing handlers

        handler = logging.StreamHandler(log_stream)
        handler.setFormatter(formatter)
        logger.addHandler(handler)

        return logger, log_stream

    def test_formatter_initialization(self):
        """Test that JSONFormatter initializes with defaults."""
        formatter = JSONFormatter()

        assert formatter.include_traceback is True
        assert formatter.include_source is True
        assert isinstance(formatter.fmt_keys, dict)

    def test_formatter_custom_keys(self):
        """Test JSONFormatter with custom key mapping."""
        formatter = JSONFormatter(fmt_keys={"time": "asctime", "severity": "levelname"})

        assert "time" in formatter.fmt_keys
        assert "severity" in formatter.fmt_keys

    def test_basic_log_formatting(self, logger_with_formatter):
        """Test basic log message formatting."""
        logger, stream = logger_with_formatter

        logger.info("Test message")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Test message"
        assert log_dict["level"] == "INFO"
        assert "timestamp" in log_dict
        assert "logger" in log_dict

    def test_log_with_multiple_levels(self, logger_with_formatter):
        """Test logging at different levels."""
        logger, stream = logger_with_formatter

        logger.debug("Debug message")
        logger.info("Info message")
        logger.warning("Warning message")
        logger.error("Error message")

        output = stream.getvalue()
        lines = [line for line in output.strip().split("\n") if line]

        assert len(lines) == 4

        for line in lines:
            log_dict = json.loads(line)
            assert "message" in log_dict
            assert "level" in log_dict

    def test_log_with_context_fields(self, logger_with_formatter):
        """Test logging with extra context fields."""
        logger, stream = logger_with_formatter

        logger.info(
            "Processing record",
            extra={"record_id": 123, "table": "users", "batch_id": "batch-456"},
        )

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Processing record"
        assert "context" in log_dict
        assert log_dict["context"]["record_id"] == 123
        assert log_dict["context"]["table"] == "users"
        assert log_dict["context"]["batch_id"] == "batch-456"

    def test_log_with_exception(self, logger_with_formatter):
        """Test logging with exception information."""
        logger, stream = logger_with_formatter

        try:
            raise ValueError("Test error")
        except ValueError:
            logger.error("An error occurred", exc_info=True)

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "An error occurred"
        assert log_dict["level"] == "ERROR"
        assert "exception" in log_dict
        assert log_dict["exception"]["type"] == "ValueError"
        assert log_dict["exception"]["message"] == "Test error"
        assert "traceback" in log_dict["exception"]

    def test_log_without_traceback(self):
        """Test logging without traceback inclusion."""
        formatter = JSONFormatter(include_traceback=False)
        stream = io.StringIO()

        logger = logging.getLogger("test_no_traceback")
        logger.setLevel(logging.DEBUG)
        logger.handlers = []

        handler = logging.StreamHandler(stream)
        handler.setFormatter(formatter)
        logger.addHandler(handler)

        try:
            raise ValueError("Test error")
        except ValueError:
            logger.error("An error occurred", exc_info=True)

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert "exception" in log_dict
        assert "traceback" not in log_dict["exception"]

    def test_log_without_source(self):
        """Test logging without source location."""
        formatter = JSONFormatter(include_source=False)
        stream = io.StringIO()

        logger = logging.getLogger("test_no_source")
        logger.setLevel(logging.DEBUG)
        logger.handlers = []

        handler = logging.StreamHandler(stream)
        handler.setFormatter(formatter)
        logger.addHandler(handler)

        logger.info("Test message")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert "source" not in log_dict

    def test_log_with_source(self, logger_with_formatter):
        """Test logging with source location information."""
        logger, stream = logger_with_formatter

        logger.info("Test message")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert "source" in log_dict
        assert "module" in log_dict["source"]
        assert "function" in log_dict["source"]
        assert "line" in log_dict["source"]
        assert "pathname" in log_dict["source"]

    def test_log_with_thread_info(self, logger_with_formatter):
        """Test that thread information is included."""
        logger, stream = logger_with_formatter

        logger.info("Test message")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert "thread" in log_dict
        assert "id" in log_dict["thread"]
        assert "name" in log_dict["thread"]

        assert "process" in log_dict
        assert "id" in log_dict["process"]


class TestStructuredLogger:
    """Tests for StructuredLogger wrapper."""

    @pytest.fixture
    def log_stream(self):
        """Create a string stream for capturing log output."""
        return io.StringIO()

    @pytest.fixture
    def setup_logger(self, log_stream):
        """Set up a logger with JSON formatter."""
        logger_name = "test_structured"
        logger = logging.getLogger(logger_name)
        logger.setLevel(logging.DEBUG)
        logger.handlers = []

        handler = logging.StreamHandler(log_stream)
        handler.setFormatter(JSONFormatter())
        logger.addHandler(handler)

        structured = StructuredLogger(logger_name)
        return structured, log_stream

    def test_structured_logger_initialization(self):
        """Test that StructuredLogger initializes correctly."""
        logger = StructuredLogger("my_module")

        assert logger.logger.name == "my_module"

    def test_info_logging(self, setup_logger):
        """Test info level logging."""
        logger, stream = setup_logger

        logger.info("Info message", user_id=123, action="login")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Info message"
        assert log_dict["level"] == "INFO"
        assert log_dict["context"]["user_id"] == 123
        assert log_dict["context"]["action"] == "login"

    def test_debug_logging(self, setup_logger):
        """Test debug level logging."""
        logger, stream = setup_logger

        logger.debug("Debug message", debug_info="test")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Debug message"
        assert log_dict["level"] == "DEBUG"

    def test_warning_logging(self, setup_logger):
        """Test warning level logging."""
        logger, stream = setup_logger

        logger.warning("Warning message", issue="deprecated_api")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Warning message"
        assert log_dict["level"] == "WARNING"
        assert log_dict["context"]["issue"] == "deprecated_api"

    def test_error_logging(self, setup_logger):
        """Test error level logging."""
        logger, stream = setup_logger

        logger.error("Error message", error_code=500)

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Error message"
        assert log_dict["level"] == "ERROR"
        assert log_dict["context"]["error_code"] == 500

    def test_error_with_exception(self, setup_logger):
        """Test error logging with exception info."""
        logger, stream = setup_logger

        try:
            raise RuntimeError("Test exception")
        except RuntimeError:
            import sys

            logger.error("Operation failed", exc_info=sys.exc_info())

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Operation failed"
        assert log_dict["level"] == "ERROR"
        assert "exception" in log_dict
        assert log_dict["exception"]["type"] == "RuntimeError"

    def test_critical_logging(self, setup_logger):
        """Test critical level logging."""
        logger, stream = setup_logger

        logger.critical("Critical message", system="database")

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert log_dict["message"] == "Critical message"
        assert log_dict["level"] == "CRITICAL"


class TestConfigureStructuredLogging:
    """Tests for configure_structured_logging function."""

    def test_configure_structured_logging(self):
        """Test that configure_structured_logging sets up root logger."""
        # Save original handlers
        root_logger = logging.getLogger()
        original_handlers = root_logger.handlers[:]
        original_level = root_logger.level

        try:
            configure_structured_logging(level=logging.INFO)

            # Root logger should have our handler
            assert len(root_logger.handlers) > 0

            # Handler should have JSONFormatter
            handler = root_logger.handlers[0]
            assert isinstance(handler.formatter, JSONFormatter)

            # Level should be set
            assert root_logger.level == logging.INFO

        finally:
            # Restore original configuration
            root_logger.handlers = original_handlers
            root_logger.level = original_level

    def test_configure_with_custom_options(self):
        """Test configuration with custom options."""
        root_logger = logging.getLogger()
        original_handlers = root_logger.handlers[:]
        original_level = root_logger.level

        try:
            configure_structured_logging(
                level=logging.DEBUG, include_traceback=False, include_source=False
            )

            handler = root_logger.handlers[0]
            formatter = handler.formatter

            assert isinstance(formatter, JSONFormatter)
            assert formatter.include_traceback is False
            assert formatter.include_source is False

        finally:
            root_logger.handlers = original_handlers
            root_logger.level = original_level


class TestGetLogger:
    """Tests for get_logger function."""

    def test_get_logger_returns_structured_logger(self):
        """Test that get_logger returns a StructuredLogger."""
        logger = get_logger("my_module")

        assert isinstance(logger, StructuredLogger)
        assert logger.logger.name == "my_module"

    def test_get_logger_different_names(self):
        """Test getting loggers with different names."""
        logger1 = get_logger("module1")
        logger2 = get_logger("module2")

        assert logger1.logger.name == "module1"
        assert logger2.logger.name == "module2"
        assert logger1.logger.name != logger2.logger.name


class TestJSONFormatCompliance:
    """Tests to ensure JSON output is valid and parseable."""

    def test_all_logs_are_valid_json(self):
        """Test that all log levels produce valid JSON."""
        stream = io.StringIO()
        logger = logging.getLogger("test_json_valid")
        logger.setLevel(logging.DEBUG)
        logger.handlers = []

        handler = logging.StreamHandler(stream)
        handler.setFormatter(JSONFormatter())
        logger.addHandler(handler)

        # Log at all levels
        logger.debug("Debug")
        logger.info("Info")
        logger.warning("Warning")
        logger.error("Error")
        logger.critical("Critical")

        output = stream.getvalue()
        lines = [line for line in output.strip().split("\n") if line]

        # All should be valid JSON
        for line in lines:
            log_dict = json.loads(line)
            assert isinstance(log_dict, dict)
            assert "message" in log_dict

    def test_special_characters_in_logs(self):
        """Test logging with special characters."""
        stream = io.StringIO()
        logger = logging.getLogger("test_special_chars")
        logger.setLevel(logging.DEBUG)
        logger.handlers = []

        handler = logging.StreamHandler(stream)
        handler.setFormatter(JSONFormatter())
        logger.addHandler(handler)

        # Log with special characters
        logger.info(
            'Message with "quotes" and \n newlines \t tabs',
            extra={"field": 'value with "quotes"', "path": "C:\\Windows\\System32"},
        )

        output = stream.getvalue()
        log_dict = json.loads(output.strip())

        assert "quotes" in log_dict["message"]
        assert "context" in log_dict
