"""
Tests to cover missing lines and edge cases.
"""

import asyncio
import json
import tempfile
from io import StringIO
import pytest

from lmlog import LLMLogger
from lmlog.backends import FileBackend, StreamBackend, AsyncFileBackend
from lmlog.decorators import capture_errors, log_performance, log_calls
from lmlog.sampling import AlwaysSampler


class TestMissingCoverage:
    """Test cases specifically designed to cover missing lines."""

    def test_logger_caller_info_edge_cases(self):
        """Test edge cases in _get_caller_info."""
        logger = LLMLogger(
            output=StringIO(), async_processing=False, sampler=AlwaysSampler()
        )

        # Test with skip_frames that goes beyond stack depth
        caller_info = logger._get_caller_info(skip_frames=100)
        assert caller_info["file"] == "unknown"
        assert caller_info["function"] == "unknown"
        assert caller_info["line"] == 0

    def test_logger_disabled_no_write(self):
        """Test that disabled logger doesn't write events."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            enabled=False,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        # Logging when disabled should not write anything
        logger.log_event("test", context={"test": True})

        output.seek(0)
        content = output.getvalue()
        assert content == ""  # No output when disabled

        # The _write_event method is an internal method and should not be called directly
        # when testing the enabled/disabled state of the logger.
        # The log_event method already handles the enabled/disabled check.
        # This test ensures that when the logger is disabled, no events are written.

    def test_logger_file_auto_flush(self):
        """Test logger file output with auto_flush=True."""
        with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as tmp:
            temp_path = tmp.name

        logger = LLMLogger(
            temp_path,
            async_processing=False,
            buffer_size=0,
            auto_flush=True,
            sampler=AlwaysSampler(),
        )

        # Add events
        logger.log_event("test1", context={"test": 1})
        logger.log_event("test2", context={"test": 2})

        # Ensure buffer is flushed
        logger.flush_buffer()

        # Read the file content
        with open(temp_path, "r") as f:
            content = f.read()

        lines = content.strip().split("\n") if content.strip() else []
        # Events should be flushed to file with auto_flush=True
        assert len(lines) >= 1
        assert "test1" in content or "test2" in content

        # Clean up
        import os

        os.unlink(temp_path)

    def test_logger_exception_conditions(self):
        """Test exception handling edge cases in logger."""
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
            LLMLogger(
                output=tmp.name,
                auto_flush=False,
                async_processing=False,
                sampler=AlwaysSampler(),
            )

            # Test buffer size 0 (no buffering)
            # These properties are read-only and cannot be set directly.
            # The logger is initialized with buffer_size=0 and auto_flush=True by default
            # if not specified, or can be set during initialization.
            # To test these conditions, the logger should be initialized with the desired values.
            logger_no_buffer = LLMLogger(
                output=tmp.name,
                buffer_size=0,
                async_processing=False,
                sampler=AlwaysSampler(),
            )
            logger_no_buffer.log_event("test_no_buffer")

            logger_no_auto_flush = LLMLogger(
                output=tmp.name,
                auto_flush=False,
                buffer_size=10,  # Set a buffer size to test auto_flush
                async_processing=False,
                sampler=AlwaysSampler(),
            )
            logger_no_auto_flush.log_event("test_no_auto_flush")
            # Manually flush to ensure it's written
            logger_no_auto_flush.flush_buffer()

    def test_logger_edge_cases(self):
        """Test edge cases in LLMLogger."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            buffer_size=0,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        # Test log_state_change with None values
        logger.log_state_change(
            entity_type="user",
            entity_id="123",
            field="status",
            before=None,
            after=None,
            trigger="test",
        )

        # Test that disabled logger state can be toggled
        logger._enabled = False
        logger._enabled = True

        # Test disabled logger doesn't write events (line 148)
        logger._enabled = False
        logger.log_event("disabled_test")
        logger._enabled = True

        output.seek(0)
        content = output.getvalue()
        # Should not contain the disabled_test event
        assert "disabled_test" not in content

    def test_logger_buffering(self):
        """Test LLMLogger buffering functionality (lines 180-184, 195-202)."""
        output = StringIO()
        # Set buffer_size to 2 to trigger buffering code paths
        logger = LLMLogger(
            output=output,
            buffer_size=2,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        # Add one event - should not flush yet
        logger.log_event("event1")

        # Add second event - should trigger buffer flush (lines 180-184)
        logger.log_event("event2")

        # Check that events were written (lines 195-202)
        output.seek(0)
        content = output.getvalue()
        assert "event1" in content
        assert "event2" in content

        # Test manual flush with empty buffer
        logger.flush_buffer()  # Should handle empty buffer gracefully

    def test_protocol_ellipsis_statements(self):
        """Test Protocol abstract methods with ellipsis statements."""
        from lmlog.backends import LogBackend

        # Create a minimal class that implements the protocol
        class MinimalBackend(LogBackend):
            def write(self, event):
                return super().write(event)  # This will execute the ... in Protocol

            def flush(self):
                return super().flush()  # This will execute the ... in Protocol

            def close(self):
                return super().close()  # This will execute the ... in Protocol

        backend = MinimalBackend()

        # Execute the protocol methods to hit the ellipsis statements
        result1 = backend.write({})  # Should return None (from ...)
        result2 = backend.flush()  # Should return None (from ...)
        result3 = backend.close()  # Should return None (from ...)

        assert result1 is None
        assert result2 is None
        assert result3 is None

    def test_backends_json_fallback(self):
        """Test backends with custom encoder."""

        # Create a custom encoder
        class BasicEncoder:
            def encode(self, obj):
                return json.dumps(obj).encode("utf-8")

            def encode_str(self, obj):
                return json.dumps(obj)

        # Test FileBackend with custom encoder
        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            backend = FileBackend(tmp.name, encoder="orjson", async_writes=False)
            # Replace encoder with custom one
            backend._encoder = BasicEncoder()

            event = {"test": "fallback"}
            backend.write(event)

            # Verify it wrote correctly
            with open(tmp.name, "r") as f:
                content = f.read().strip()
                parsed = json.loads(content)
                assert parsed == event

        # Test StreamBackend with custom encoder
        import io

        stream = io.StringIO()
        backend = StreamBackend(stream, encoder="orjson")
        backend._encoder = BasicEncoder()

        event = {"test": "stream_fallback"}
        backend.write(event)

        stream.seek(0)
        content = stream.read().strip()
        parsed = json.loads(content)
        assert parsed == event

    @pytest.mark.asyncio
    async def test_async_backend_json_fallback(self):
        """Test AsyncFileBackend with custom encoder."""

        class BasicEncoder:
            def encode(self, obj):
                return json.dumps(obj).encode("utf-8")

            def encode_str(self, obj):
                return json.dumps(obj)

        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            backend = AsyncFileBackend(tmp.name, encoder="orjson")
            backend._encoder = BasicEncoder()

            await backend.start()

            event = {"test": "async_fallback"}
            await backend.write(event)

            # Wait for processing
            await asyncio.sleep(0.1)
            await backend.stop()

            # Verify it wrote correctly
            with open(tmp.name, "r") as f:
                content = f.read().strip()
                if content:
                    parsed = json.loads(content)
                    assert parsed == event

    def test_logger_kwargs_filtering(self):
        """Test kwargs filtering in logger."""
        logger = LLMLogger(
            output=StringIO(), async_processing=False, sampler=AlwaysSampler()
        )

        # Test with kwargs that include None values
        logger.log_event(
            event_type="test",
            operation="test_op",
            none_value=None,  # This should be filtered
            valid_value="keep_this",
        )

    @pytest.mark.asyncio
    async def test_async_file_backend_stop_when_not_running(self):
        """Test AsyncFileBackend stop when not running."""
        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            backend = AsyncFileBackend(tmp.name)

            # Call stop without starting
            await backend.stop()  # Should handle gracefully

    @pytest.mark.asyncio
    async def test_async_file_backend_close_with_done_task(self):
        """Test AsyncFileBackend close with completed task."""
        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            backend = AsyncFileBackend(tmp.name)
            await backend.start()
            await backend.stop()

            # Task should be done now
            backend.close()  # Should handle gracefully

    @pytest.mark.asyncio
    async def test_log_performance_async_decorator(self):
        """Test log_performance decorator with async function."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            async_processing=False,
            buffer_size=0,
            auto_flush=True,
            sampler=AlwaysSampler(),
        )

        @log_performance(logger, threshold_ms=1, log_all=True)
        async def async_function():
            await asyncio.sleep(0.001)
            return "result"

        result = await async_function()
        assert result == "result"

        output.seek(0)
        content = output.getvalue()
        assert "performance_info" in content or "performance_issue" in content

    @pytest.mark.asyncio
    async def test_capture_errors_async_decorator(self):
        """Test capture_errors decorator with async function."""
        output = StringIO()
        logger = LLMLogger(
            output=output, async_processing=False, buffer_size=0, auto_flush=True
        )

        @capture_errors(logger, include_args=True)
        async def async_failing_function(arg1, kwarg1="test"):
            raise ValueError("Async test error")

        with pytest.raises(ValueError):
            await async_failing_function("value1", kwarg1="kwvalue")

        output.seek(0)
        content = output.getvalue()
        assert "exception" in content
        assert "Async test error" in content

    @pytest.mark.asyncio
    async def test_log_calls_async_decorator_with_exception(self):
        """Test log_calls decorator with async function that raises exception."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            async_processing=False,
            buffer_size=0,
            auto_flush=True,
            sampler=AlwaysSampler(),
        )

        @log_calls(logger, include_result=True)
        async def async_failing_function():
            raise RuntimeError("Async function failed")

        with pytest.raises(RuntimeError):
            await async_failing_function()

        output.seek(0)
        log_lines = output.getvalue().strip().split("\n")

        # Should have entry and error exit logs
        assert len(log_lines) >= 2
        assert "function_entry" in log_lines[0]
        assert "function_exit_error" in log_lines[1]

    def test_log_calls_with_result_length(self):
        """Test log_calls decorator with result that has length."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            buffer_size=0,
            auto_flush=True,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        @log_calls(logger, include_result=True)
        def function_with_list_result():
            return [1, 2, 3, 4, 5]

        result = function_with_list_result()
        assert len(result) == 5

        output.seek(0)
        content = output.getvalue()
        assert "result_length" in content
        assert "5" in content

    def test_log_calls_with_result_type(self):
        """Test log_calls decorator with result type logging."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            buffer_size=0,
            auto_flush=True,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        @log_calls(logger, include_result=True)
        def function_with_dict_result():
            return {"key": "value"}

        result = function_with_dict_result()
        assert result == {"key": "value"}

        output.seek(0)
        content = output.getvalue()
        assert "result_type" in content
        assert "dict" in content

    def test_logger_context_manager_exception(self):
        """Test logger context manager exception handling."""
        output = StringIO()

        try:
            with LLMLogger(
                output=output,
                async_processing=False,
                buffer_size=0,
                auto_flush=True,
                sampler=AlwaysSampler(),
            ) as logger:
                logger.log_event("test")
                raise ValueError("Test exception in context")
        except ValueError:
            pass  # Expected

        # Should have flushed on exit despite exception
        output.seek(0)
        content = output.getvalue()
        assert "test" in content

    def test_logger_operation_context_exception(self):
        """Test logger operation context with exception."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            buffer_size=0,
            auto_flush=True,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        try:
            with logger.operation_context("test_operation", key="value"):
                logger.log_event("inside_operation")
                raise ValueError("Test exception")
        except ValueError:
            pass  # Expected

        output.seek(0)
        content = output.getvalue()
        lines = content.strip().split("\n")

        # Should have operation_start, inside event, and operation_error
        assert len(lines) >= 3
        assert any("operation_start" in line for line in lines)
        assert any("inside_operation" in line for line in lines)
        assert any("operation_error" in line for line in lines)

    @pytest.mark.asyncio
    async def test_log_calls_async_with_args_and_result(self):
        """Test async log_calls decorator with include_args and include_result."""
        output = StringIO()
        logger = LLMLogger(
            output=output,
            buffer_size=0,
            auto_flush=True,
            async_processing=False,
            sampler=AlwaysSampler(),
        )

        @log_calls(logger, include_args=True, include_result=True)
        async def async_function_with_list(arg1, arg2, kwarg1="test"):
            await asyncio.sleep(0.001)
            return [1, 2, 3, 4]  # Result with __len__

        result = await async_function_with_list("value1", "value2", kwarg1="kwvalue")
        assert len(result) == 4

        output.seek(0)
        content = output.getvalue()

        # Should include args and result info
        assert "args_count" in content
        assert "kwargs_keys" in content
        assert "result_type" in content
        assert "result_length" in content
        assert "4" in content  # Length of result list
