"""Tests for rbsp.indexer.wal -- Write-Ahead Log with crash recovery."""

import os
import struct
import sys
import tempfile
import threading
import unittest
import zlib

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.indexer.wal import (
    WAL_MAGIC,
    WAL_VERSION,
    WAL_HEADER_SIZE,
    WALOperation,
    SyncMode,
    WALConfig,
    WALEntry,
    WriteAheadLog,
    _pack_entry,
    _unpack_entry,
    _pack_header,
    _unpack_header,
    _ENTRY_SENTINEL,
)


# ---------------------------------------------------------------------------
# WALConfig
# ---------------------------------------------------------------------------

class TestWALConfig(unittest.TestCase):
    """Tests for WALConfig frozen dataclass."""

    def test_defaults(self):
        cfg = WALConfig(log_dir="/tmp/wal")
        self.assertEqual(cfg.sync_mode, SyncMode.FSYNC)
        self.assertEqual(cfg.max_log_size, 64 * 1024 * 1024)
        self.assertEqual(cfg.checkpoint_interval, 1000)

    def test_custom_values(self):
        cfg = WALConfig(
            log_dir="/tmp/custom",
            sync_mode=SyncMode.NONE,
            max_log_size=1024,
            checkpoint_interval=50,
        )
        self.assertEqual(cfg.log_dir, "/tmp/custom")
        self.assertEqual(cfg.sync_mode, SyncMode.NONE)
        self.assertEqual(cfg.max_log_size, 1024)
        self.assertEqual(cfg.checkpoint_interval, 50)

    def test_frozen_cannot_mutate(self):
        cfg = WALConfig(log_dir="/tmp/wal")
        with self.assertRaises(AttributeError):
            cfg.max_log_size = 999


# ---------------------------------------------------------------------------
# WALOperation Enum
# ---------------------------------------------------------------------------

class TestWALOperation(unittest.TestCase):
    """Tests for WALOperation IntEnum."""

    def test_values(self):
        self.assertEqual(WALOperation.INSERT, 1)
        self.assertEqual(WALOperation.DELETE, 2)
        self.assertEqual(WALOperation.UPDATE, 3)
        self.assertEqual(WALOperation.CHECKPOINT, 4)
        self.assertEqual(WALOperation.SEGMENT_CREATE, 5)
        self.assertEqual(WALOperation.SEGMENT_MERGE, 6)
        self.assertEqual(WALOperation.SEGMENT_DELETE, 7)

    def test_all_operations_defined(self):
        self.assertEqual(len(WALOperation), 7)


# ---------------------------------------------------------------------------
# SyncMode Enum
# ---------------------------------------------------------------------------

class TestSyncMode(unittest.TestCase):
    """Tests for SyncMode enum."""

    def test_values(self):
        self.assertEqual(SyncMode.FSYNC.value, "fsync")
        self.assertEqual(SyncMode.FDATASYNC.value, "fdatasync")
        self.assertEqual(SyncMode.NONE.value, "none")


# ---------------------------------------------------------------------------
# Entry Packing / Unpacking
# ---------------------------------------------------------------------------

class TestEntryPacking(unittest.TestCase):
    """Tests for _pack_entry / _unpack_entry round-trip."""

    def test_round_trip_with_data(self):
        packed, crc = _pack_entry(WALOperation.INSERT, 1700000000.5, b'{"doc":"abc"}')
        entry, end = _unpack_entry(packed, 0, sequence=42)
        self.assertIsNotNone(entry)
        self.assertEqual(entry.sequence, 42)
        self.assertEqual(entry.operation, WALOperation.INSERT)
        self.assertAlmostEqual(entry.timestamp, 1700000000.5, places=3)
        self.assertEqual(entry.data, b'{"doc":"abc"}')
        self.assertEqual(entry.checksum, crc)
        self.assertEqual(end, len(packed))

    def test_round_trip_empty_data(self):
        packed, crc = _pack_entry(WALOperation.CHECKPOINT, 1.0, b"")
        entry, _ = _unpack_entry(packed, 0, sequence=0)
        self.assertIsNotNone(entry)
        self.assertEqual(entry.operation, WALOperation.CHECKPOINT)
        self.assertEqual(entry.data, b"")

    def test_round_trip_all_operations(self):
        for op in WALOperation:
            packed, crc = _pack_entry(op, 2.0, b"test")
            entry, _ = _unpack_entry(packed, 0, sequence=0)
            self.assertIsNotNone(entry, f"Failed for operation {op}")
            self.assertEqual(entry.operation, op)

    def test_multiple_entries(self):
        e1, _ = _pack_entry(WALOperation.INSERT, 1.0, b"aaa")
        e2, _ = _pack_entry(WALOperation.DELETE, 2.0, b"bb")
        e3, _ = _pack_entry(WALOperation.UPDATE, 3.0, b"c")
        data = e1 + e2 + e3

        entries = []
        offset = 0
        seq = 0
        while offset < len(data):
            entry, new_offset = _unpack_entry(data, offset, seq)
            if entry is None:
                break
            entries.append(entry)
            seq += 1
            offset = new_offset

        self.assertEqual(len(entries), 3)
        self.assertEqual(entries[0].operation, WALOperation.INSERT)
        self.assertEqual(entries[1].operation, WALOperation.DELETE)
        self.assertEqual(entries[2].operation, WALOperation.UPDATE)

    def test_truncated_returns_none(self):
        packed, _ = _pack_entry(WALOperation.INSERT, 1.0, b"data")
        truncated = packed[:5]
        entry, offset = _unpack_entry(truncated, 0, sequence=0)
        self.assertIsNone(entry)
        self.assertEqual(offset, 0)

    def test_truncated_payload_returns_none(self):
        packed, _ = _pack_entry(WALOperation.INSERT, 1.0, b"data" * 100)
        truncated = packed[:20]  # within header, before full payload
        entry, offset = _unpack_entry(truncated, 0, sequence=0)
        self.assertIsNone(entry)

    def test_corrupted_crc_returns_none(self):
        packed = bytearray(_pack_entry(WALOperation.INSERT, 1.0, b"data")[0])
        packed[-1] ^= 0xFF  # flip CRC byte
        entry, offset = _unpack_entry(bytes(packed), 0, sequence=0)
        self.assertIsNone(entry)

    def test_corrupted_payload_detected_by_crc(self):
        packed = bytearray(_pack_entry(WALOperation.INSERT, 1.0, b"important")[0])
        packed[15] ^= 0x42  # corrupt a byte in the payload
        entry, offset = _unpack_entry(bytes(packed), 0, sequence=0)
        self.assertIsNone(entry)

    def test_bad_sentinel_returns_none(self):
        packed = bytearray(_pack_entry(WALOperation.INSERT, 1.0, b"data")[0])
        packed[0:4] = b"\x00\x00\x00\x00"  # break sentinel
        entry, offset = _unpack_entry(bytes(packed), 0, sequence=0)
        self.assertIsNone(entry)

    def test_large_payload(self):
        big_data = b"x" * 100_000
        packed, crc = _pack_entry(WALOperation.INSERT, 5.0, big_data)
        entry, _ = _unpack_entry(packed, 0, sequence=99)
        self.assertIsNotNone(entry)
        self.assertEqual(entry.data, big_data)


# ---------------------------------------------------------------------------
# WAL Header Packing
# ---------------------------------------------------------------------------

class TestHeaderPacking(unittest.TestCase):
    """Tests for WAL file header pack/unpack."""

    def test_round_trip(self):
        header = _pack_header(sequence_start=42)
        result = _unpack_header(header)
        self.assertIsNotNone(result)
        version, seq_start = result
        self.assertEqual(version, WAL_VERSION)
        self.assertEqual(seq_start, 42)

    def test_bad_magic_returns_none(self):
        header = bytearray(_pack_header(0))
        header[0:4] = b"XXXX"
        result = _unpack_header(bytes(header))
        self.assertIsNone(result)

    def test_truncated_returns_none(self):
        header = _pack_header(0)
        result = _unpack_header(header[:8])
        self.assertIsNone(result)

    def test_header_size(self):
        header = _pack_header(0)
        self.assertEqual(len(header), WAL_HEADER_SIZE)


# ---------------------------------------------------------------------------
# WriteAheadLog -- Basic Operations
# ---------------------------------------------------------------------------

class TestWriteAheadLogBasic(unittest.TestCase):
    """Tests for basic WAL write operations."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        self.cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        self.wal = WriteAheadLog(self.cfg)

    def tearDown(self):
        self.wal.close()

    def test_append_returns_sequence(self):
        seq = self.wal.append(WALOperation.INSERT, b'{"doc":"a"}')
        self.assertEqual(seq, 0)

    def test_sequence_increments(self):
        s0 = self.wal.append(WALOperation.INSERT, b"a")
        s1 = self.wal.append(WALOperation.DELETE, b"b")
        s2 = self.wal.append(WALOperation.UPDATE, b"c")
        self.assertEqual(s0, 0)
        self.assertEqual(s1, 1)
        self.assertEqual(s2, 2)

    def test_entries_since_checkpoint(self):
        self.assertEqual(self.wal.entries_since_checkpoint, 0)
        self.wal.append(WALOperation.INSERT, b"a")
        self.assertEqual(self.wal.entries_since_checkpoint, 1)
        self.wal.append(WALOperation.INSERT, b"b")
        self.assertEqual(self.wal.entries_since_checkpoint, 2)

    def test_len_returns_entries_since_checkpoint(self):
        self.assertEqual(len(self.wal), 0)
        self.wal.append(WALOperation.INSERT, b"a")
        self.assertEqual(len(self.wal), 1)

    def test_current_sequence_property(self):
        self.assertEqual(self.wal.current_sequence, 0)
        self.wal.append(WALOperation.INSERT, b"a")
        self.assertEqual(self.wal.current_sequence, 1)

    def test_contains_sequence(self):
        seq = self.wal.append(WALOperation.INSERT, b"a")
        self.assertIn(seq, self.wal)
        self.assertNotIn(999, self.wal)

    def test_repr(self):
        r = repr(self.wal)
        self.assertIn("WriteAheadLog", r)
        self.assertIn("sequence=", r)

    def test_context_manager(self):
        with WriteAheadLog(WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)) as wal:
            wal.append(WALOperation.INSERT, b"ctx")
        with self.assertRaises(RuntimeError):
            wal.append(WALOperation.INSERT, b"fail")

    def test_empty_data_payload(self):
        seq = self.wal.append(WALOperation.INSERT)
        self.assertEqual(seq, 0)

    def test_all_operation_types(self):
        for op in WALOperation:
            if op == WALOperation.CHECKPOINT:
                continue  # checkpoint has its own method
            seq = self.wal.append(op, b"test")
            self.assertGreaterEqual(seq, 0)

    def test_wal_file_created(self):
        wal_files = list(self.wal._get_wal_files())
        self.assertGreaterEqual(len(wal_files), 1)
        self.assertTrue(wal_files[0].name.startswith("wal_"))
        self.assertTrue(wal_files[0].name.endswith(".log"))


# ---------------------------------------------------------------------------
# WriteAheadLog -- Checkpoint
# ---------------------------------------------------------------------------

class TestWriteAheadLogCheckpoint(unittest.TestCase):
    """Tests for checkpoint and marker file."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        self.cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        self.wal = WriteAheadLog(self.cfg)

    def tearDown(self):
        self.wal.close()

    def test_checkpoint_creates_marker_file(self):
        self.wal.append(WALOperation.INSERT, b"a")
        self.wal.checkpoint()
        marker_path = os.path.join(self.tmpdir, "checkpoint")
        self.assertTrue(os.path.exists(marker_path))

    def test_checkpoint_returns_sequence(self):
        self.wal.append(WALOperation.INSERT, b"a")
        seq = self.wal.checkpoint()
        self.assertEqual(seq, 1)  # seq 0 = INSERT, seq 1 = CHECKPOINT

    def test_checkpoint_resets_entry_count(self):
        for i in range(5):
            self.wal.append(WALOperation.INSERT, f"doc{i}".encode())
        self.assertEqual(self.wal.entries_since_checkpoint, 5)
        self.wal.checkpoint()
        self.assertEqual(self.wal.entries_since_checkpoint, 0)

    def test_checkpoint_marker_contains_sequence(self):
        self.wal.append(WALOperation.INSERT, b"a")
        ckpt_seq = self.wal.checkpoint()
        marker_path = os.path.join(self.tmpdir, "checkpoint")
        with open(marker_path, "rb") as f:
            (stored_seq,) = struct.unpack(">Q", f.read(8))
        self.assertEqual(stored_seq, ckpt_seq)

    def test_no_tmp_file_after_checkpoint(self):
        self.wal.append(WALOperation.INSERT, b"a")
        self.wal.checkpoint()
        tmp_path = os.path.join(self.tmpdir, "checkpoint.tmp")
        self.assertFalse(os.path.exists(tmp_path))

    def test_write_after_checkpoint(self):
        self.wal.append(WALOperation.INSERT, b"a")
        ckpt_seq = self.wal.checkpoint()
        new_seq = self.wal.append(WALOperation.INSERT, b"b")
        self.assertGreater(new_seq, ckpt_seq)
        self.assertEqual(self.wal.entries_since_checkpoint, 1)

    def test_checkpoint_with_snapshot_data(self):
        self.wal.append(WALOperation.INSERT, b"a")
        snapshot = b'{"entry_count": 1, "segment_ids": ["seg1"]}'
        seq = self.wal.checkpoint(snapshot_data=snapshot)
        self.assertGreaterEqual(seq, 0)


# ---------------------------------------------------------------------------
# WriteAheadLog -- Recovery
# ---------------------------------------------------------------------------

class TestWriteAheadLogRecovery(unittest.TestCase):
    """Tests for crash recovery via WAL replay."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def test_recover_empty_wal(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        entries = wal.recover()
        self.assertEqual(entries, [])
        wal.close()

    def test_recover_replays_all_without_checkpoint(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b'{"doc":"a"}')
        wal.append(WALOperation.INSERT, b'{"doc":"b"}')
        wal.append(WALOperation.DELETE, b'{"doc":"a"}')
        wal.close()

        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 3)
        self.assertEqual(entries[0].operation, WALOperation.INSERT)
        self.assertEqual(entries[1].operation, WALOperation.INSERT)
        self.assertEqual(entries[2].operation, WALOperation.DELETE)
        wal2.close()

    def test_recover_skips_before_checkpoint(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")
        wal.append(WALOperation.INSERT, b"b")
        wal.append(WALOperation.INSERT, b"c")
        wal.checkpoint()

        wal.append(WALOperation.INSERT, b"d")
        wal.append(WALOperation.DELETE, b"b")
        wal.close()

        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        # Only entries after checkpoint should be recovered
        self.assertEqual(len(entries), 2)
        self.assertEqual(entries[0].data, b"d")
        self.assertEqual(entries[1].data, b"b")
        wal2.close()

    def test_recover_continues_sequence(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")
        wal.append(WALOperation.INSERT, b"b")
        wal.close()

        wal2 = WriteAheadLog(cfg)
        wal2.recover()
        seq = wal2.append(WALOperation.INSERT, b"c")
        self.assertEqual(seq, 2)  # continues from 0, 1 -> 2
        wal2.close()

    def test_recover_from_checkpoint_continues_sequence(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        for i in range(5):
            wal.append(WALOperation.INSERT, f"{i}".encode())
        wal.checkpoint()  # seq 5
        wal.append(WALOperation.INSERT, b"post")  # seq 6
        wal.close()

        wal2 = WriteAheadLog(cfg)
        wal2.recover()
        seq = wal2.append(WALOperation.INSERT, b"new")
        self.assertEqual(seq, 7)
        wal2.close()


# ---------------------------------------------------------------------------
# WriteAheadLog -- Rotation
# ---------------------------------------------------------------------------

class TestWriteAheadLogRotation(unittest.TestCase):
    """Tests for WAL file rotation."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def test_manual_rotate_creates_new_file(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")

        files_before = list(wal._get_wal_files())
        wal.rotate()
        files_after = list(wal._get_wal_files())

        self.assertEqual(len(files_after), len(files_before) + 1)
        wal.close()

    def test_auto_rotate_on_max_log_size(self):
        cfg = WALConfig(
            log_dir=self.tmpdir,
            sync_mode=SyncMode.NONE,
            max_log_size=100,  # very small to trigger rotation
        )
        wal = WriteAheadLog(cfg)

        # Write enough to exceed 100 bytes
        for i in range(10):
            wal.append(WALOperation.INSERT, b"x" * 50)

        files = list(wal._get_wal_files())
        self.assertGreater(len(files), 1)
        wal.close()

    def test_sequence_continues_across_rotation(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        s0 = wal.append(WALOperation.INSERT, b"a")
        s1 = wal.append(WALOperation.INSERT, b"b")
        wal.rotate()
        s2 = wal.append(WALOperation.INSERT, b"c")

        self.assertEqual(s0, 0)
        self.assertEqual(s1, 1)
        self.assertEqual(s2, 2)
        wal.close()

    def test_recovery_across_multiple_files(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"file1_a")
        wal.append(WALOperation.INSERT, b"file1_b")
        wal.rotate()
        wal.append(WALOperation.INSERT, b"file2_a")
        wal.append(WALOperation.DELETE, b"file2_b")
        wal.close()

        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 4)
        self.assertEqual(entries[0].data, b"file1_a")
        self.assertEqual(entries[3].data, b"file2_b")
        wal2.close()


# ---------------------------------------------------------------------------
# WriteAheadLog -- Truncation
# ---------------------------------------------------------------------------

class TestWriteAheadLogTruncation(unittest.TestCase):
    """Tests for WAL file truncation."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def test_truncate_deletes_old_files(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")
        wal.append(WALOperation.INSERT, b"b")
        wal.rotate()
        wal.append(WALOperation.INSERT, b"c")
        wal.rotate()
        wal.append(WALOperation.INSERT, b"d")

        files_before = len(wal._get_wal_files())
        deleted = wal.truncate(up_to_sequence=2)
        files_after = len(wal._get_wal_files())

        self.assertGreater(deleted, 0)
        self.assertLess(files_after, files_before)
        wal.close()

    def test_truncate_does_not_delete_current(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")

        deleted = wal.truncate(up_to_sequence=999)
        self.assertEqual(deleted, 0)

        files = list(wal._get_wal_files())
        self.assertGreaterEqual(len(files), 1)
        wal.close()

    def test_truncate_returns_count(self):
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        deleted = wal.truncate(up_to_sequence=0)
        self.assertEqual(deleted, 0)
        wal.close()


# ---------------------------------------------------------------------------
# WriteAheadLog -- Simulated Crash Scenarios
# ---------------------------------------------------------------------------

class TestWriteAheadLogCrash(unittest.TestCase):
    """Tests simulating crashes by modifying WAL files directly."""

    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()

    def test_crash_mid_write_truncated_entry(self):
        """Simulate a crash that truncates the last entry."""
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"first")
        wal.append(WALOperation.INSERT, b"second")
        wal.close()

        # Truncate the WAL file
        wal_files = sorted(wal._log_dir.glob("wal_*.log"))
        data = wal_files[-1].read_bytes()
        truncated = data[:-10]
        with open(wal_files[-1], "wb") as f:
            f.write(truncated)

        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 1)
        self.assertEqual(entries[0].data, b"first")
        wal2.close()

    def test_crash_corrupted_crc_in_middle(self):
        """Simulate corruption of an entry's CRC."""
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")
        wal.append(WALOperation.INSERT, b"b")
        wal.append(WALOperation.INSERT, b"c")
        wal.close()

        # Corrupt a byte in the second entry
        wal_files = sorted(wal._log_dir.glob("wal_*.log"))
        data = bytearray(wal_files[-1].read_bytes())

        # Find end of first entry by parsing
        _, first_end = _unpack_entry(bytes(data), WAL_HEADER_SIZE, 0)
        # Corrupt the second entry
        data[first_end + 10] ^= 0xFF

        with open(wal_files[-1], "wb") as f:
            f.write(data)

        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 1)  # only first entry survives
        wal2.close()

    def test_crash_empty_wal_file(self):
        """Simulate a crash that left an empty WAL file."""
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)

        # Create a WAL file with just the header
        wal_path = os.path.join(self.tmpdir, "wal_0000000000000000.log")
        with open(wal_path, "wb") as f:
            f.write(_pack_header(0))

        wal = WriteAheadLog(cfg)
        entries = wal.recover()
        self.assertEqual(entries, [])
        wal.close()

    def test_crash_corrupted_checkpoint_marker(self):
        """Simulate a corrupted checkpoint marker -- recovery should still work."""
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.append(WALOperation.INSERT, b"a")
        wal.append(WALOperation.INSERT, b"b")
        wal.close()

        # Write garbage to checkpoint file
        marker_path = os.path.join(self.tmpdir, "checkpoint")
        with open(marker_path, "wb") as f:
            f.write(b"GARBAGE")

        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 2)
        wal2.close()

    def test_crash_only_sentinel_written(self):
        """Simulate a crash where only the entry sentinel was written."""
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)

        wal_path = os.path.join(self.tmpdir, "wal_0000000000000000.log")
        with open(wal_path, "wb") as f:
            f.write(_pack_header(0))
            # Write just a partial sentinel
            f.write(struct.pack(">I", _ENTRY_SENTINEL))

        wal = WriteAheadLog(cfg)
        entries = wal.recover()
        self.assertEqual(entries, [])
        wal.close()

    def test_crash_recovery_with_valid_checkpoint(self):
        """Full checkpoint + crash + recovery scenario."""
        cfg = WALConfig(log_dir=self.tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)

        # Phase 1: write entries and checkpoint
        wal.append(WALOperation.INSERT, b"old1")
        wal.append(WALOperation.INSERT, b"old2")
        wal.checkpoint()

        # Phase 2: write more entries, then "crash" (close without checkpoint)
        wal.append(WALOperation.INSERT, b"new1")
        wal.append(WALOperation.DELETE, b"old1")
        wal.close()

        # Phase 3: recover
        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 2)
        self.assertEqual(entries[0].data, b"new1")
        self.assertEqual(entries[1].data, b"old1")
        wal2.close()


# ---------------------------------------------------------------------------
# WriteAheadLog -- Thread Safety
# ---------------------------------------------------------------------------

class TestWriteAheadLogThreadSafety(unittest.TestCase):
    """Tests for concurrent WAL writes."""

    def test_concurrent_appends(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)

        errors = []
        sequences = []
        seq_lock = threading.Lock()

        def writer(thread_id: int):
            try:
                for i in range(20):
                    seq = wal.append(
                        WALOperation.INSERT,
                        f'{{"thread":{thread_id},"i":{i}}}'.encode(),
                    )
                    with seq_lock:
                        sequences.append(seq)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=writer, args=(t,)) for t in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [])
        self.assertEqual(len(sequences), 100)
        self.assertEqual(len(set(sequences)), 100)  # all unique

        # All entries recoverable
        entries = wal.recover()
        self.assertEqual(len(entries), 100)
        wal.close()


# ---------------------------------------------------------------------------
# WriteAheadLog -- Lifecycle / Edge Cases
# ---------------------------------------------------------------------------

class TestWriteAheadLogLifecycle(unittest.TestCase):
    """Tests for lifecycle and error handling."""

    def test_close_is_idempotent(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.close()
        wal.close()  # should not raise

    def test_append_after_close_raises(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.close()
        with self.assertRaises(RuntimeError):
            wal.append(WALOperation.INSERT, b"fail")

    def test_flush_after_close_raises(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.close()
        with self.assertRaises(RuntimeError):
            wal.flush()

    def test_checkpoint_after_close_raises(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.close()
        with self.assertRaises(RuntimeError):
            wal.checkpoint()

    def test_rotate_after_close_raises(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.close()
        with self.assertRaises(RuntimeError):
            wal.rotate()

    def test_truncate_after_close_raises(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        wal.close()
        with self.assertRaises(RuntimeError):
            wal.truncate(up_to_sequence=0)

    def test_log_dir_created_if_not_exists(self):
        tmpdir = tempfile.mkdtemp()
        log_dir = os.path.join(tmpdir, "sub", "nested")
        cfg = WALConfig(log_dir=log_dir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)
        self.assertTrue(os.path.isdir(log_dir))
        wal.close()

    def test_multiple_checkpoints(self):
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.NONE)
        wal = WriteAheadLog(cfg)

        wal.append(WALOperation.INSERT, b"a")
        ckpt1 = wal.checkpoint()
        wal.append(WALOperation.INSERT, b"b")
        ckpt2 = wal.checkpoint()

        self.assertGreater(ckpt2, ckpt1)

        wal.append(WALOperation.INSERT, b"c")
        wal.close()

        # Only post-ckpt2 entries recovered
        wal2 = WriteAheadLog(cfg)
        entries = wal2.recover()
        self.assertEqual(len(entries), 1)
        self.assertEqual(entries[0].data, b"c")
        wal2.close()

    def test_sync_mode_fsync(self):
        """Test that SyncMode.FSYNC works without crashing."""
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.FSYNC)
        wal = WriteAheadLog(cfg)
        seq = wal.append(WALOperation.INSERT, b"synced")
        self.assertEqual(seq, 0)
        wal.close()

    def test_sync_mode_fdatasync(self):
        """Test that SyncMode.FDATASYNC works (falls back on Windows)."""
        tmpdir = tempfile.mkdtemp()
        cfg = WALConfig(log_dir=tmpdir, sync_mode=SyncMode.FDATASYNC)
        wal = WriteAheadLog(cfg)
        seq = wal.append(WALOperation.INSERT, b"synced")
        self.assertEqual(seq, 0)
        wal.close()

    def test_wal_entry_is_frozen(self):
        entry = WALEntry(
            sequence=0, operation=WALOperation.INSERT,
            timestamp=1.0, data=b"x", checksum=0,
        )
        with self.assertRaises(AttributeError):
            entry.sequence = 99


if __name__ == "__main__":
    unittest.main()
