"""
Tests for RotatingFileSink error handling.

Scope:
- Write error containment
- Flush error containment
- Start error handling
- Stop error handling
- Strict envelope mode error handling
- Fallback write methods
- Write segments exception handling
- Text mode fallback handling
- File operations exception handling
- List rotated files error handling
- Stringify exception handling
"""

import asyncio
from pathlib import Path

import pytest

from fapilog.plugins.sinks.rotating_file import (
    RotatingFileSink,
    RotatingFileSinkConfig,
)


@pytest.mark.asyncio
async def test_write_error_is_contained(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    class BrokenFile:
        def write(self, _seg):  # type: ignore[no-untyped-def]
            raise OSError("disk full")

        def flush(self) -> None:
            pass

        def close(self) -> None:
            pass

    async def fake_open_new(self) -> None:  # type: ignore[no-redef]
        self._active_path = Path(tmp_path / "broken.jsonl")
        self._active_file = BrokenFile()  # type: ignore[assignment]
        self._active_size = 0
        self._next_rotation_deadline = None

    monkeypatch.setattr(
        "fapilog.plugins.sinks.rotating_file.RotatingFileSink._open_new_file",
        fake_open_new,
        raising=True,
    )

    sink = RotatingFileSink(
        RotatingFileSinkConfig(directory=tmp_path, max_bytes=1024, mode="json")
    )
    await sink.start()
    try:
        # Should not raise even if underlying write fails
        await sink.write({"a": 1})
    finally:
        await sink.stop()


@pytest.mark.asyncio
async def test_flush_error_is_contained(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    class FlushBrokenFile:
        def __init__(self) -> None:
            self._buf = bytearray()

        def write(self, seg):  # type: ignore[no-untyped-def]
            # seg is a memoryview; accept and append
            self._buf.extend(seg)

        def flush(self) -> None:
            raise OSError("flush failed")

        def close(self) -> None:
            pass

    async def fake_open_new(self) -> None:  # type: ignore[no-redef]
        self._active_path = Path(tmp_path / "flushbroken.jsonl")
        self._active_file = FlushBrokenFile()  # type: ignore[assignment]
        self._active_size = 0
        self._next_rotation_deadline = None

    monkeypatch.setattr(
        "fapilog.plugins.sinks.rotating_file.RotatingFileSink._open_new_file",
        fake_open_new,
        raising=True,
    )

    sink = RotatingFileSink(
        RotatingFileSinkConfig(directory=tmp_path, max_bytes=1024, mode="json")
    )
    await sink.start()
    try:
        # Should not raise even if flush fails
        await sink.write({"a": 1})
    finally:
        await sink.stop()


@pytest.mark.asyncio
async def test_start_error_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that start() handles initialization errors gracefully."""

    async def failing_mkdir(*args, **kwargs):
        raise OSError("Permission denied")

    monkeypatch.setattr("asyncio.to_thread", failing_mkdir)

    cfg = RotatingFileSinkConfig(directory=tmp_path)
    sink = RotatingFileSink(cfg)

    # Should not raise, should handle error gracefully
    result = await sink.start()
    assert result is None


@pytest.mark.asyncio
async def test_stop_error_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that stop() handles cleanup errors gracefully."""

    class BrokenFile:
        def flush(self):
            raise OSError("flush failed")

        def close(self):
            raise OSError("close failed")

    async def fake_open_new(self):
        self._active_path = Path(tmp_path / "broken.jsonl")
        self._active_file = BrokenFile()
        self._active_size = 0
        self._next_rotation_deadline = None

    monkeypatch.setattr(
        "fapilog.plugins.sinks.rotating_file.RotatingFileSink._open_new_file",
        fake_open_new,
        raising=True,
    )

    cfg = RotatingFileSinkConfig(directory=tmp_path)
    sink = RotatingFileSink(cfg)
    await sink.start()

    # Should not raise, should handle error gracefully
    result = await sink.stop()
    assert result is None


@pytest.mark.asyncio
async def test_strict_envelope_mode_error_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test strict envelope mode error handling in write()."""

    # Mock settings to return strict mode
    class MockSettings:
        class Core:
            strict_envelope_mode = True

        core = Core()

    monkeypatch.setattr("fapilog.core.settings.Settings", lambda: MockSettings())

    # Mock serialize_envelope to fail
    def failing_serialize(*args, **kwargs):
        raise ValueError("Invalid envelope")

    monkeypatch.setattr(
        "fapilog.plugins.sinks.rotating_file.serialize_envelope",
        failing_serialize,
    )

    cfg = RotatingFileSinkConfig(directory=tmp_path, mode="json")
    sink = RotatingFileSink(cfg)
    await sink.start()

    try:
        # Should return None due to strict mode
        result = await sink.write({"invalid": "data"})
        assert result is None
    finally:
        await sink.stop()


@pytest.mark.asyncio
async def test_fallback_write_methods(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test fallback write methods when os.writev is not available or fails."""

    class FallbackFile:
        def __init__(self):
            self._buf = bytearray()
            self._fileno_called = False

        def fileno(self):
            self._fileno_called = True
            return 999  # Invalid fd

        def writelines(self, segments):
            for seg in segments:
                self._buf.extend(seg)

        def write(self, data):
            self._buf.extend(data)

        def flush(self):
            pass

        def close(self):
            pass

        def get_content(self):
            return bytes(self._buf)

    async def fake_open_new(self):
        self._active_path = Path(tmp_path / "fallback.jsonl")
        self._active_file = FallbackFile()
        self._active_size = 0
        self._next_rotation_deadline = None

    monkeypatch.setattr(
        "fapilog.plugins.sinks.rotating_file.RotatingFileSink._open_new_file",
        fake_open_new,
        raising=True,
    )

    cfg = RotatingFileSinkConfig(directory=tmp_path, mode="json")
    sink = RotatingFileSink(cfg)
    await sink.start()

    try:
        await sink.write({"test": "data"})

        # Verify fallback write was used
        assert sink._active_file._fileno_called
        content = sink._active_file.get_content()
        assert b"test" in content
        assert b"data" in content
    finally:
        await sink.stop()


@pytest.mark.asyncio
async def test_write_segments_exception_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that write segment exceptions are handled gracefully."""

    class ExceptionFile:
        def __init__(self):
            self._buf = bytearray()

        def fileno(self):
            raise OSError("fileno failed")

        def writelines(self, segments):
            raise OSError("writelines failed")

        def write(self, data):
            raise OSError("write failed")

        def flush(self):
            raise OSError("flush failed")

        def close(self):
            pass

        def get_content(self):
            return bytes(self._buf)

    async def fake_open_new(self):
        self._active_path = Path(tmp_path / "exception.jsonl")
        self._active_file = ExceptionFile()
        self._active_size = 0
        self._next_rotation_deadline = None

    monkeypatch.setattr(
        "fapilog.plugins.sinks.rotating_file.RotatingFileSink._open_new_file",
        fake_open_new,
        raising=True,
    )

    cfg = RotatingFileSinkConfig(directory=tmp_path, mode="json")
    sink = RotatingFileSink(cfg)
    await sink.start()

    try:
        # Should not raise, should handle all write exceptions gracefully
        result = await sink.write({"test": "data"})
        assert result is None
    finally:
        await sink.stop()


@pytest.mark.asyncio
async def test_text_mode_fallback_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test text mode fallback when sorting fails."""

    class UnsortableDict(dict):
        def items(self):
            raise TypeError("Cannot sort")

    cfg = RotatingFileSinkConfig(directory=tmp_path, mode="text")
    sink = RotatingFileSink(cfg)
    await sink.start()

    try:
        # Should handle unsortable dict gracefully
        await sink.write(UnsortableDict({"key": "value"}))

        # Verify fallback message was written
        files = [p for p in tmp_path.iterdir() if p.is_file()]
        assert files
        with open(files[0], "rb") as f:
            content = f.read().decode("utf-8")
            assert "message=" in content
    finally:
        await sink.stop()


@pytest.mark.asyncio
async def test_open_new_file_stat_exception(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test _open_new_file handles stat exceptions gracefully."""

    # Mock only the specific stat call, not all asyncio.to_thread calls
    original_to_thread = asyncio.to_thread

    async def mock_to_thread(func, *args, **kwargs):
        if func.__name__ == "stat":
            raise OSError("stat failed")
        return await original_to_thread(func, *args, **kwargs)

    monkeypatch.setattr("asyncio.to_thread", mock_to_thread)

    cfg = RotatingFileSinkConfig(directory=tmp_path)
    sink = RotatingFileSink(cfg)

    # Should handle stat failure gracefully
    await sink._open_new_file()
    assert sink._active_size == 0


@pytest.mark.asyncio
async def test_rotate_active_file_no_active_file(tmp_path: Path) -> None:
    """Test _rotate_active_file when no active file exists."""

    cfg = RotatingFileSinkConfig(directory=tmp_path)
    sink = RotatingFileSink(cfg)

    # Should handle gracefully and open new file
    await sink._rotate_active_file()
    assert hasattr(sink._active_file, "write")  # Verify it's a writable file object
    # Verify the file was actually created on disk
    assert hasattr(sink._active_path, "exists") and sink._active_path.exists()


@pytest.mark.asyncio
async def test_compress_file_exception_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test _compress_file handles compression failures gracefully."""

    # Create a test file to compress
    test_file = tmp_path / "test.jsonl"
    test_file.write_text('{"test": "data"}\n')

    def failing_compress(*args, **kwargs):
        raise OSError("compression failed")

    monkeypatch.setattr("asyncio.to_thread", failing_compress)

    cfg = RotatingFileSinkConfig(directory=tmp_path, compress_rotated=True)
    sink = RotatingFileSink(cfg)

    # Should handle compression failure gracefully
    await sink._compress_file(test_file)

    # Original file should remain
    assert test_file.exists()


@pytest.mark.asyncio
async def test_enforce_retention_exception_handling(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test _enforce_retention handles exceptions gracefully."""

    def failing_list_files(*args, **kwargs):
        raise OSError("list failed")

    monkeypatch.setattr("asyncio.to_thread", failing_list_files)

    cfg = RotatingFileSinkConfig(directory=tmp_path)
    sink = RotatingFileSink(cfg)

    # Should handle list failure gracefully
    await sink._enforce_retention()


@pytest.mark.asyncio
async def test_enforce_retention_unlink_exceptions(tmp_path: Path) -> None:
    """Test _enforce_retention handles unlink exceptions gracefully."""

    # Create some test files
    test_files = []
    for i in range(3):
        test_file = tmp_path / f"test-{i}.jsonl"
        test_file.write_text(f'{{"test": {i}}}\n')
        test_files.append(test_file)

    cfg = RotatingFileSinkConfig(directory=tmp_path, max_files=1)
    sink = RotatingFileSink(cfg)

    # Mock _list_rotated_files to return our test files
    def mock_list_files():
        return test_files

    sink._list_rotated_files = mock_list_files

    # Should handle unlink exceptions gracefully
    await sink._enforce_retention()


@pytest.mark.asyncio
async def test_enforce_retention_max_total_bytes_exceptions(tmp_path: Path) -> None:
    """Test _enforce_retention handles stat exceptions in max_total_bytes logic."""

    # Create some test files
    test_files = []
    for i in range(3):
        test_file = tmp_path / f"test-{i}.jsonl"
        test_file.write_text(f'{{"test": {i}}}\n')
        test_files.append(test_file)

    cfg = RotatingFileSinkConfig(directory=tmp_path, max_total_bytes=10)
    sink = RotatingFileSink(cfg)

    # Mock _list_rotated_files to return our test files
    def mock_list_files():
        return test_files

    sink._list_rotated_files = mock_list_files

    # Should handle stat exceptions gracefully
    await sink._enforce_retention()


@pytest.mark.asyncio
async def test_list_rotated_files_directory_not_exists(tmp_path: Path) -> None:
    """Test _list_rotated_files when directory doesn't exist."""

    cfg = RotatingFileSinkConfig(directory=tmp_path / "nonexistent")
    sink = RotatingFileSink(cfg)

    # Should handle non-existent directory gracefully
    result = sink._list_rotated_files()
    assert result == []


@pytest.mark.asyncio
async def test_list_rotated_files_iterdir_exception(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test _list_rotated_files handles iterdir exceptions gracefully."""

    cfg = RotatingFileSinkConfig(directory=tmp_path)
    sink = RotatingFileSink(cfg)

    # Create a directory that will fail on iterdir
    class BrokenPath:
        def exists(self) -> bool:
            return True

        def iterdir(self):
            raise OSError("iterdir failed")

    sink._cfg.directory = BrokenPath()

    # Should handle iterdir failure gracefully
    result = sink._list_rotated_files()
    assert result == []


@pytest.mark.asyncio
async def test_stringify_exception_handling() -> None:
    """Test _stringify handles string conversion exceptions gracefully."""

    cfg = RotatingFileSinkConfig(directory=Path("/tmp"))
    sink = RotatingFileSink(cfg)

    class Unstringable:
        def __str__(self):
            raise ValueError("Cannot convert to string")

    # Should handle string conversion failure gracefully
    result = sink._stringify(Unstringable())
    assert result == "<?>"
