import unittest
import os
import random
import string

from preseem.ringbuffer import RingBuffer, SHARED_MEMORY_AVAILABLE

@unittest.skipIf(not SHARED_MEMORY_AVAILABLE, "RingBuffer requires Python 3.8+ (shared_memory not available)")
class TestRingBuffer(unittest.TestCase):

    def setUp(self):
        """
        Create a temporary ring buffer for each test,
        so tests are isolated and do not interfere.
        """
        # Generate a random shared memory name to avoid collisions
        self.buffer_name = "test_ring_" + "".join(random.choices(string.ascii_lowercase, k=8))
        self.capacity = 256
        self.ring = RingBuffer(name=self.buffer_name, capacity=self.capacity, create=True)

    def tearDown(self):
        """
        Clean up shared memory at the end of each test.
        """
        self.ring.close()
        self.ring.unlink()

    def test_empty_read_returns_none(self):
        """Reading from an empty buffer should return None."""
        self.assertIsNone(self.ring._read(), "Expected None when reading from empty ring buffer")

    def test_single_message(self):
        """Write a single message, then read it back."""
        msg = b"Hello, World!"
        self.ring._write(msg)
        read_back = self.ring._read()
        self.assertEqual(read_back, msg, "Message read should match the message written")

    def test_multiple_small_messages(self):
        """Write multiple small messages and verify read order."""
        messages = [b"msg1", b"msg2", b"msg3"]
        for m in messages:
            self.ring._write(m)

        for expected in messages:
            actual = self.ring._read()
            self.assertEqual(actual, expected)

        # Ring should be empty now
        self.assertIsNone(self.ring._read())

    def test_wraparound_write(self):
        """
        Force the buffer to wrap around by writing enough data so
        the tail crosses the physical end, then verify messages.
        """
        # We'll use a small capacity (256) to force wrap
        msg1 = b"A" * 100   # 100 bytes
        msg2 = b"B" * 100   # Another 100 bytes
        msg3 = b"C" * 40    # 40 bytes => total ~240 bytes used

        self.ring._write(msg1)
        self.ring._write(msg2)
        self.ring._write(msg3)

        # Now reading them should still give us the correct content:
        self.assertEqual(self.ring._read(), msg1)
        self.assertEqual(self.ring._read(), msg2)
        self.assertEqual(self.ring._read(), msg3)
        self.assertIsNone(self.ring._read(), "Buffer should be empty after reading all messages")

    def test_discarding_old_messages(self):
        """
        Ensure that old messages are discarded when there's not enough room.
        We'll create a scenario where multiple old messages must be discarded.
        """
        # Fill buffer nearly to capacity with multiple small messages
        msg_size = 30
        count = (self.capacity // (msg_size + 4))  # each message has 4-byte prefix
        for i in range(count):
            data = (f"msg{i}").encode().ljust(msg_size, b"x")
            self.ring._write(data)

        # Now attempt to write a larger message that won't fit by discarding just one old message
        large_msg = b"L" * 100
        self.ring._write(large_msg)  # Should discard enough old messages

        # Read until we find our large message
        found_large_msg = False
        while True:
            result = self.ring._read()
            if result is None:
                break
            if result == large_msg:
                found_large_msg = True

        self.assertTrue(found_large_msg, "Failed to find the large message; discard logic might be incorrect")

    def test_message_too_large(self):
        """Try writing a message larger than the entire capacity."""
        too_big = b"A" * (self.capacity + 1)
        with self.assertRaises(ValueError):
            self.ring._write(too_big)

    def test_corrupted_message_size_detection(self):
        """Test that reading corrupted message size is detected."""
        import struct
        # Write a valid message first
        msg = b"valid message"
        self.ring._write(msg)

        # Manually corrupt the message size prefix to an invalid huge value
        # The prefix is at META_SIZE + head (which is 0)
        self.ring._attach()
        corrupt_size = self.capacity + 1000  # Larger than capacity
        struct.pack_into('>I', self.ring.shm.buf, self.ring.META_SIZE, corrupt_size)

        # Reading should now raise ValueError
        with self.assertRaises(ValueError, msg="Should detect corrupted message size"):
            self.ring._read()

    def test_zero_message_size_detection(self):
        """Test that zero message size is detected as corruption."""
        import struct
        # Write a valid message first
        msg = b"valid message"
        self.ring._write(msg)

        # Manually corrupt the message size prefix to zero
        self.ring._attach()
        struct.pack_into('>I', self.ring.shm.buf, self.ring.META_SIZE, 0)

        # Reading should now raise ValueError
        with self.assertRaises(ValueError, msg="Should detect zero message size"):
            self.ring._read()
