"""Tests for rbsp.indexer.mmap_store -- MMapStore persistent key-value store."""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(__file__))
from conftest import RBSPTestCase, FIXED_TIMESTAMP, SAMPLE_TEXTS

from rbsp.indexer.mmap_store import MMapStore


class TestMMapStoreBasics(RBSPTestCase):
    """Core put/get/delete semantics."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("test.ridx")
        self.store = MMapStore(self.store_path)

    def tearDown(self):
        self.store.close()
        super().tearDown()

    def test_empty_store(self):
        self.assertEqual(len(self.store), 0)
        self.assertIsNone(self.store.get("missing"))

    def test_put_and_get(self):
        self.store.put("key1", b"value1")
        result = self.store.get("key1")
        self.assertEqual(result, b"value1")

    def test_put_multiple(self):
        self.store.put("a", b"1")
        self.store.put("b", b"2")
        self.store.put("c", b"3")
        self.assertEqual(self.store.get("a"), b"1")
        self.assertEqual(self.store.get("b"), b"2")
        self.assertEqual(self.store.get("c"), b"3")
        self.assertEqual(len(self.store), 3)

    def test_get_missing_key(self):
        self.assertIsNone(self.store.get("nonexistent"))

    def test_delete_existing(self):
        self.store.put("key1", b"value1")
        result = self.store.delete("key1")
        self.assertTrue(result)
        self.assertIsNone(self.store.get("key1"))
        self.assertEqual(len(self.store), 0)

    def test_delete_nonexistent(self):
        result = self.store.delete("nope")
        self.assertFalse(result)

    def test_contains(self):
        self.store.put("present", b"yes")
        self.assertIn("present", self.store)
        self.assertNotIn("absent", self.store)

    def test_len(self):
        self.assertEqual(len(self.store), 0)
        self.store.put("a", b"1")
        self.assertEqual(len(self.store), 1)
        self.store.put("b", b"2")
        self.assertEqual(len(self.store), 2)
        self.store.delete("a")
        self.assertEqual(len(self.store), 1)

    def test_overwrite_existing_key(self):
        self.store.put("key1", b"original")
        self.store.put("key1", b"updated")
        self.assertEqual(self.store.get("key1"), b"updated")
        self.assertEqual(len(self.store), 1)


class TestMMapStoreIteration(RBSPTestCase):
    """Tests for keys() and items()."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("iter.ridx")
        self.store = MMapStore(self.store_path)

    def tearDown(self):
        self.store.close()
        super().tearDown()

    def test_keys_empty(self):
        self.assertEqual(list(self.store.keys()), [])

    def test_keys(self):
        self.store.put("alpha", b"1")
        self.store.put("beta", b"2")
        self.store.put("gamma", b"3")
        keys = set(self.store.keys())
        self.assertEqual(keys, {"alpha", "beta", "gamma"})

    def test_items_empty(self):
        self.assertEqual(list(self.store.items()), [])

    def test_items(self):
        self.store.put("x", b"10")
        self.store.put("y", b"20")
        items = dict(self.store.items())
        self.assertEqual(items, {"x": b"10", "y": b"20"})


class TestMMapStorePersistence(RBSPTestCase):
    """Data survives close and reopen."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("persist.ridx")

    def test_data_persists_after_close_reopen(self):
        store = MMapStore(self.store_path)
        store.put("key1", b"value1")
        store.put("key2", b"value2")
        store.close()

        store2 = MMapStore(self.store_path)
        try:
            self.assertEqual(store2.get("key1"), b"value1")
            self.assertEqual(store2.get("key2"), b"value2")
            self.assertEqual(len(store2), 2)
        finally:
            store2.close()

    def test_delete_persists(self):
        store = MMapStore(self.store_path)
        store.put("a", b"1")
        store.put("b", b"2")
        store.delete("a")
        store.close()

        store2 = MMapStore(self.store_path)
        try:
            self.assertIsNone(store2.get("a"))
            self.assertEqual(store2.get("b"), b"2")
            self.assertEqual(len(store2), 1)
        finally:
            store2.close()

    def test_overwrite_persists(self):
        store = MMapStore(self.store_path)
        store.put("k", b"v1")
        store.put("k", b"v2")
        store.close()

        store2 = MMapStore(self.store_path)
        try:
            self.assertEqual(store2.get("k"), b"v2")
            self.assertEqual(len(store2), 1)
        finally:
            store2.close()


class TestMMapStoreReadonly(RBSPTestCase):
    """Read-only mode."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("readonly.ridx")
        # Create and populate a store first
        store = MMapStore(self.store_path)
        store.put("key1", b"value1")
        store.close()

    def test_readonly_can_read(self):
        store = MMapStore(self.store_path, readonly=True)
        try:
            self.assertEqual(store.get("key1"), b"value1")
            self.assertEqual(len(store), 1)
            self.assertIn("key1", store)
        finally:
            store.close()

    def test_readonly_put_raises(self):
        store = MMapStore(self.store_path, readonly=True)
        try:
            with self.assertRaises(RuntimeError):
                store.put("key2", b"value2")
        finally:
            store.close()

    def test_readonly_delete_raises(self):
        store = MMapStore(self.store_path, readonly=True)
        try:
            with self.assertRaises(RuntimeError):
                store.delete("key1")
        finally:
            store.close()

    def test_readonly_missing_file_raises(self):
        with self.assertRaises(FileNotFoundError):
            MMapStore(self.tmp_path("does_not_exist.ridx"), readonly=True)


class TestMMapStoreContextManager(RBSPTestCase):
    """Context manager protocol."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("ctx.ridx")

    def test_context_manager(self):
        with MMapStore(self.store_path) as store:
            store.put("ctx_key", b"ctx_value")
            self.assertEqual(store.get("ctx_key"), b"ctx_value")
        # After exiting, store should be closed; verify by reopening
        with MMapStore(self.store_path) as store2:
            self.assertEqual(store2.get("ctx_key"), b"ctx_value")

    def test_context_manager_returns_self(self):
        store = MMapStore(self.store_path)
        with store as s:
            self.assertIs(s, store)


class TestMMapStoreFlush(RBSPTestCase):
    """Flush operation."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("flush.ridx")

    def test_flush_does_not_error(self):
        with MMapStore(self.store_path) as store:
            store.put("k", b"v")
            store.flush()
            # Should still be readable after flush
            self.assertEqual(store.get("k"), b"v")


class TestMMapStoreLargePayloads(RBSPTestCase):
    """Storing larger values to exercise file growth."""

    def setUp(self):
        super().setUp()
        self.store_path = self.tmp_path("large.ridx")
        self.store = MMapStore(self.store_path)

    def tearDown(self):
        self.store.close()
        super().tearDown()

    def test_many_entries(self):
        for i in range(100):
            self.store.put(f"key_{i}", f"value_{i}".encode())
        self.assertEqual(len(self.store), 100)
        for i in range(100):
            self.assertEqual(self.store.get(f"key_{i}"), f"value_{i}".encode())

    def test_large_value(self):
        big_value = b"x" * 50000
        self.store.put("big", big_value)
        self.assertEqual(self.store.get("big"), big_value)


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