"""
Multi-process tests for RingBuffer to verify IPC functionality.

These tests verify the actual use case: multiple processes communicating
via shared memory ring buffer.
"""
import unittest
import multiprocessing
import time
import random
import string

from preseem.ringbuffer import RingBuffer, SHARED_MEMORY_AVAILABLE


def writer_process(buffer_name, messages, delay=0):
    """Writer process that writes messages to the ring buffer."""
    ring = RingBuffer(name=buffer_name, capacity=1024*1024, create=True)
    try:
        for msg in messages:
            if delay:
                time.sleep(delay)
            ring._write(msg)
    finally:
        ring.close()


def reader_process(buffer_name, expected_count, result_queue, timeout=5):
    """Reader process that reads messages from the ring buffer."""
    ring = RingBuffer(name=buffer_name)
    messages = []
    start = time.time()

    try:
        while len(messages) < expected_count:
            if time.time() - start > timeout:
                break
            msg = ring._read()
            if msg:
                messages.append(msg)
            else:
                time.sleep(0.01)  # Small delay if buffer empty
    finally:
        ring.close()
        result_queue.put(messages)


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

    def setUp(self):
        """Generate unique buffer name for test isolation."""
        self.buffer_name = "test_mp_" + "".join(random.choices(string.ascii_lowercase, k=8))

    def tearDown(self):
        """Clean up shared memory."""
        try:
            ring = RingBuffer(name=self.buffer_name)
            ring.close()
            ring.unlink()
        except:
            pass  # May already be cleaned up

    def test_single_writer_single_reader(self):
        """Test basic IPC: one writer, one reader."""
        messages = [b"msg1", b"msg2", b"msg3", b"msg4", b"msg5"]
        result_queue = multiprocessing.Queue()

        # Start writer
        writer = multiprocessing.Process(
            target=writer_process,
            args=(self.buffer_name, messages)
        )
        writer.start()

        # Give writer time to create buffer and write
        time.sleep(0.1)

        # Start reader
        reader = multiprocessing.Process(
            target=reader_process,
            args=(self.buffer_name, len(messages), result_queue)
        )
        reader.start()

        # Wait for completion
        writer.join(timeout=5)
        reader.join(timeout=5)

        # Verify results
        self.assertFalse(writer.is_alive(), "Writer process hung")
        self.assertFalse(reader.is_alive(), "Reader process hung")

        received = result_queue.get(timeout=1)
        self.assertEqual(len(received), len(messages), "Not all messages received")
        self.assertEqual(received, messages, "Messages don't match")

    def test_writer_survives_reader_restart(self):
        """Test that writer can continue if reader stops/restarts."""
        messages_batch1 = [b"batch1_msg1", b"batch1_msg2"]
        messages_batch2 = [b"batch2_msg1", b"batch2_msg2"]
        result_queue = multiprocessing.Queue()

        # Start writer with first batch
        writer1 = multiprocessing.Process(
            target=writer_process,
            args=(self.buffer_name, messages_batch1)
        )
        writer1.start()
        writer1.join(timeout=5)

        # Read first batch
        reader1 = multiprocessing.Process(
            target=reader_process,
            args=(self.buffer_name, len(messages_batch1), result_queue)
        )
        reader1.start()
        reader1.join(timeout=5)

        batch1_received = result_queue.get(timeout=1)
        self.assertEqual(batch1_received, messages_batch1)

        # Write second batch (reader stopped)
        writer2 = multiprocessing.Process(
            target=writer_process,
            args=(self.buffer_name, messages_batch2)
        )
        writer2.start()
        writer2.join(timeout=5)

        # Read second batch with new reader
        reader2 = multiprocessing.Process(
            target=reader_process,
            args=(self.buffer_name, len(messages_batch2), result_queue)
        )
        reader2.start()
        reader2.join(timeout=5)

        batch2_received = result_queue.get(timeout=1)
        self.assertEqual(batch2_received, messages_batch2)

    def test_buffer_persistence_across_processes(self):
        """Test that messages persist in buffer when all processes exit."""
        messages = [b"persistent1", b"persistent2", b"persistent3"]

        # Writer writes and exits
        writer = multiprocessing.Process(
            target=writer_process,
            args=(self.buffer_name, messages)
        )
        writer.start()
        writer.join(timeout=5)
        self.assertFalse(writer.is_alive())

        # Small delay to ensure writer fully exits
        time.sleep(0.1)

        # New reader should still be able to read messages
        result_queue = multiprocessing.Queue()
        reader = multiprocessing.Process(
            target=reader_process,
            args=(self.buffer_name, len(messages), result_queue)
        )
        reader.start()
        reader.join(timeout=5)

        received = result_queue.get(timeout=1)
        self.assertEqual(received, messages, "Messages not persisted after writer exit")

    def test_concurrent_writes_sequential_reads(self):
        """Test multiple writers can write without corruption."""
        writer1_msgs = [b"w1_msg1", b"w1_msg2", b"w1_msg3"]
        writer2_msgs = [b"w2_msg1", b"w2_msg2", b"w2_msg3"]
        total_expected = len(writer1_msgs) + len(writer2_msgs)

        # Start both writers
        writer1 = multiprocessing.Process(
            target=writer_process,
            args=(self.buffer_name, writer1_msgs, 0.01)
        )
        writer2 = multiprocessing.Process(
            target=writer_process,
            args=(self.buffer_name, writer2_msgs, 0.01)
        )

        writer1.start()
        time.sleep(0.05)  # Stagger slightly
        writer2.start()

        writer1.join(timeout=5)
        writer2.join(timeout=5)

        # Read all messages
        result_queue = multiprocessing.Queue()
        reader = multiprocessing.Process(
            target=reader_process,
            args=(self.buffer_name, total_expected, result_queue, 10)
        )
        reader.start()
        reader.join(timeout=10)

        received = result_queue.get(timeout=1)

        # We should get all messages, though order may vary due to concurrency
        self.assertEqual(len(received), total_expected, "Not all messages from concurrent writers received")

        # Verify all messages from both writers are present
        for msg in writer1_msgs + writer2_msgs:
            self.assertIn(msg, received, f"Missing message: {msg}")


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