"""Tests for rbsp.runtime.migration -- IndexVersion, migrations, MigrationManager."""

from __future__ import annotations

import os
import sys
import unittest

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

from rbsp.runtime.migration import (
    CURRENT_VERSION,
    IndexVersion,
    Migration,
    MigrationManager,
    V1ToV2Migration,
    V2ToV3Migration,
    create_default_manager,
)


# ---------------------------------------------------------------------------
# IndexVersion
# ---------------------------------------------------------------------------

class TestIndexVersionFromString(unittest.TestCase):
    """IndexVersion.from_string parses 'major.minor.patch'."""

    def test_basic_parse(self):
        v = IndexVersion.from_string("1.2.3")
        self.assertEqual(v, IndexVersion(1, 2, 3))

    def test_zero_version(self):
        v = IndexVersion.from_string("0.0.0")
        self.assertEqual(v, IndexVersion(0, 0, 0))

    def test_large_numbers(self):
        v = IndexVersion.from_string("100.200.300")
        self.assertEqual(v, IndexVersion(100, 200, 300))

    def test_whitespace_stripped(self):
        v = IndexVersion.from_string("  1.0.0  ")
        self.assertEqual(v, IndexVersion(1, 0, 0))

    def test_invalid_format_two_parts(self):
        with self.assertRaises(ValueError):
            IndexVersion.from_string("1.2")

    def test_invalid_format_four_parts(self):
        with self.assertRaises(ValueError):
            IndexVersion.from_string("1.2.3.4")

    def test_invalid_non_numeric(self):
        with self.assertRaises(ValueError):
            IndexVersion.from_string("a.b.c")

    def test_invalid_empty(self):
        with self.assertRaises(ValueError):
            IndexVersion.from_string("")


class TestIndexVersionComparisons(unittest.TestCase):
    """IndexVersion comparison operators."""

    def test_equal(self):
        self.assertEqual(IndexVersion(1, 0, 0), IndexVersion(1, 0, 0))

    def test_not_equal(self):
        self.assertNotEqual(IndexVersion(1, 0, 0), IndexVersion(2, 0, 0))

    def test_less_than_major(self):
        self.assertLess(IndexVersion(1, 0, 0), IndexVersion(2, 0, 0))

    def test_less_than_minor(self):
        self.assertLess(IndexVersion(1, 0, 0), IndexVersion(1, 1, 0))

    def test_less_than_patch(self):
        self.assertLess(IndexVersion(1, 0, 0), IndexVersion(1, 0, 1))

    def test_greater_than(self):
        self.assertGreater(IndexVersion(3, 0, 0), IndexVersion(2, 0, 0))

    def test_less_equal(self):
        self.assertLessEqual(IndexVersion(1, 0, 0), IndexVersion(1, 0, 0))
        self.assertLessEqual(IndexVersion(1, 0, 0), IndexVersion(2, 0, 0))

    def test_greater_equal(self):
        self.assertGreaterEqual(IndexVersion(2, 0, 0), IndexVersion(2, 0, 0))
        self.assertGreaterEqual(IndexVersion(3, 0, 0), IndexVersion(2, 0, 0))

    def test_not_equal_to_non_version(self):
        v = IndexVersion(1, 0, 0)
        self.assertNotEqual(v, "1.0.0")
        self.assertNotEqual(v, 100)

    def test_lt_with_non_version_returns_not_implemented(self):
        v = IndexVersion(1, 0, 0)
        result = v.__lt__("not a version")
        self.assertEqual(result, NotImplemented)

    def test_gt_with_non_version_returns_not_implemented(self):
        v = IndexVersion(1, 0, 0)
        result = v.__gt__("not a version")
        self.assertEqual(result, NotImplemented)

    def test_le_with_non_version_returns_not_implemented(self):
        v = IndexVersion(1, 0, 0)
        result = v.__le__("not a version")
        self.assertEqual(result, NotImplemented)

    def test_ge_with_non_version_returns_not_implemented(self):
        v = IndexVersion(1, 0, 0)
        result = v.__ge__("not a version")
        self.assertEqual(result, NotImplemented)


class TestIndexVersionHash(unittest.TestCase):
    """IndexVersion is hashable for use in sets/dicts."""

    def test_equal_versions_same_hash(self):
        a = IndexVersion(1, 0, 0)
        b = IndexVersion(1, 0, 0)
        self.assertEqual(hash(a), hash(b))

    def test_usable_in_set(self):
        s = {IndexVersion(1, 0, 0), IndexVersion(1, 0, 0), IndexVersion(2, 0, 0)}
        self.assertEqual(len(s), 2)

    def test_usable_as_dict_key(self):
        d = {IndexVersion(1, 0, 0): "v1", IndexVersion(2, 0, 0): "v2"}
        self.assertEqual(d[IndexVersion(1, 0, 0)], "v1")


class TestIndexVersionStr(unittest.TestCase):
    """__str__ produces 'major.minor.patch'."""

    def test_str(self):
        self.assertEqual(str(IndexVersion(1, 2, 3)), "1.2.3")

    def test_str_zeros(self):
        self.assertEqual(str(IndexVersion(0, 0, 0)), "0.0.0")


# ---------------------------------------------------------------------------
# V1ToV2Migration
# ---------------------------------------------------------------------------

class TestV1ToV2Migration(unittest.TestCase):
    """V1 -> V2: adds token_set field."""

    def setUp(self):
        self.migration = V1ToV2Migration()

    def test_versions(self):
        self.assertEqual(self.migration.from_version, IndexVersion(1, 0, 0))
        self.assertEqual(self.migration.to_version, IndexVersion(2, 0, 0))

    def test_migrate_adds_token_set(self):
        data = {
            "entries": {
                "doc1": {"tokens": ["a", "b", "a", "c"]},
                "doc2": {"tokens": ["x", "y"]},
            }
        }
        result = self.migration.migrate(data)
        self.assertEqual(set(result["entries"]["doc1"]["token_set"]), {"a", "b", "c"})
        self.assertEqual(set(result["entries"]["doc2"]["token_set"]), {"x", "y"})
        self.assertEqual(result["version"], "2.0.0")

    def test_migrate_preserves_existing_token_set(self):
        data = {
            "entries": {
                "doc1": {"tokens": ["a", "b"], "token_set": ["z"]},
            }
        }
        result = self.migration.migrate(data)
        self.assertEqual(result["entries"]["doc1"]["token_set"], ["z"])

    def test_migrate_empty_entries(self):
        data = {"entries": {}}
        result = self.migration.migrate(data)
        self.assertEqual(result["entries"], {})
        self.assertEqual(result["version"], "2.0.0")

    def test_validate_pass(self):
        data = {
            "entries": {
                "doc1": {"token_set": ["a"]},
            }
        }
        errors = self.migration.validate(data)
        self.assertEqual(errors, [])

    def test_validate_fail(self):
        data = {
            "entries": {
                "doc1": {"tokens": ["a"]},
            }
        }
        errors = self.migration.validate(data)
        self.assertEqual(len(errors), 1)
        self.assertIn("doc1", errors[0])


# ---------------------------------------------------------------------------
# V2ToV3Migration
# ---------------------------------------------------------------------------

class TestV2ToV3Migration(unittest.TestCase):
    """V2 -> V3: adds term_freqs and doc_len."""

    def setUp(self):
        self.migration = V2ToV3Migration()

    def test_versions(self):
        self.assertEqual(self.migration.from_version, IndexVersion(2, 0, 0))
        self.assertEqual(self.migration.to_version, IndexVersion(3, 0, 0))

    def test_migrate_adds_fields(self):
        data = {
            "entries": {
                "doc1": {"tokens": ["hello", "world", "hello"]},
            }
        }
        result = self.migration.migrate(data)
        entry = result["entries"]["doc1"]
        self.assertEqual(entry["term_freqs"], {"hello": 2, "world": 1})
        self.assertEqual(entry["doc_len"], 3)
        self.assertEqual(result["version"], "3.0.0")

    def test_migrate_preserves_existing_fields(self):
        data = {
            "entries": {
                "doc1": {
                    "tokens": ["a"],
                    "term_freqs": {"custom": 5},
                    "doc_len": 99,
                },
            }
        }
        result = self.migration.migrate(data)
        self.assertEqual(result["entries"]["doc1"]["term_freqs"], {"custom": 5})
        self.assertEqual(result["entries"]["doc1"]["doc_len"], 99)

    def test_migrate_empty_tokens(self):
        data = {
            "entries": {
                "doc1": {"tokens": []},
            }
        }
        result = self.migration.migrate(data)
        self.assertEqual(result["entries"]["doc1"]["term_freqs"], {})
        self.assertEqual(result["entries"]["doc1"]["doc_len"], 0)

    def test_validate_pass(self):
        data = {
            "entries": {
                "doc1": {"term_freqs": {}, "doc_len": 0},
            }
        }
        errors = self.migration.validate(data)
        self.assertEqual(errors, [])

    def test_validate_fail_missing_both(self):
        data = {
            "entries": {
                "doc1": {"tokens": ["a"]},
            }
        }
        errors = self.migration.validate(data)
        self.assertEqual(len(errors), 2)


# ---------------------------------------------------------------------------
# MigrationManager
# ---------------------------------------------------------------------------

class TestMigrationManagerNeedsMigration(unittest.TestCase):
    """needs_migration checks if version is old."""

    def test_needs_migration_true(self):
        mgr = MigrationManager(IndexVersion(3, 0, 0))
        self.assertTrue(mgr.needs_migration(IndexVersion(1, 0, 0)))
        self.assertTrue(mgr.needs_migration(IndexVersion(2, 0, 0)))

    def test_needs_migration_false_at_current(self):
        mgr = MigrationManager(IndexVersion(3, 0, 0))
        self.assertFalse(mgr.needs_migration(IndexVersion(3, 0, 0)))

    def test_needs_migration_false_above_current(self):
        mgr = MigrationManager(IndexVersion(3, 0, 0))
        self.assertFalse(mgr.needs_migration(IndexVersion(4, 0, 0)))


class TestMigrationManagerGetPath(unittest.TestCase):
    """get_migration_path finds ordered chain of migrations."""

    def test_single_step(self):
        mgr = MigrationManager(IndexVersion(2, 0, 0))
        mgr.register(V1ToV2Migration())
        path = mgr.get_migration_path(IndexVersion(1, 0, 0))
        self.assertEqual(len(path), 1)
        self.assertIsInstance(path[0], V1ToV2Migration)

    def test_multi_step(self):
        mgr = MigrationManager(IndexVersion(3, 0, 0))
        mgr.register(V1ToV2Migration())
        mgr.register(V2ToV3Migration())
        path = mgr.get_migration_path(IndexVersion(1, 0, 0))
        self.assertEqual(len(path), 2)
        self.assertIsInstance(path[0], V1ToV2Migration)
        self.assertIsInstance(path[1], V2ToV3Migration)

    def test_empty_path_at_current(self):
        mgr = MigrationManager(IndexVersion(3, 0, 0))
        path = mgr.get_migration_path(IndexVersion(3, 0, 0))
        self.assertEqual(path, [])

    def test_no_path_raises(self):
        mgr = MigrationManager(IndexVersion(5, 0, 0))
        mgr.register(V1ToV2Migration())
        with self.assertRaises(ValueError):
            mgr.get_migration_path(IndexVersion(1, 0, 0))


class TestMigrationManagerMigrate(unittest.TestCase):
    """migrate applies all steps."""

    def test_full_migration_v1_to_v3(self):
        mgr = create_default_manager()
        data = {
            "entries": {
                "doc1": {"tokens": ["hello", "world", "hello"]},
            }
        }
        result = mgr.migrate(data, IndexVersion(1, 0, 0))
        entry = result["entries"]["doc1"]
        self.assertIn("token_set", entry)
        self.assertIn("term_freqs", entry)
        self.assertIn("doc_len", entry)
        self.assertEqual(result["version"], "3.0.0")

    def test_migrate_from_v2(self):
        mgr = create_default_manager()
        data = {
            "entries": {
                "doc1": {"tokens": ["a", "b"], "token_set": ["a", "b"]},
            }
        }
        result = mgr.migrate(data, IndexVersion(2, 0, 0))
        entry = result["entries"]["doc1"]
        self.assertEqual(entry["term_freqs"], {"a": 1, "b": 1})
        self.assertEqual(entry["doc_len"], 2)

    def test_no_migration_at_current(self):
        mgr = create_default_manager()
        data = {"entries": {"doc1": {"all": "present"}}}
        result = mgr.migrate(data, CURRENT_VERSION)
        self.assertIs(result, data)  # unchanged, same object


# ---------------------------------------------------------------------------
# create_default_manager
# ---------------------------------------------------------------------------

class TestCreateDefaultManager(unittest.TestCase):
    """create_default_manager returns a pre-configured manager."""

    def test_current_version(self):
        mgr = create_default_manager()
        self.assertEqual(mgr.current_version, CURRENT_VERSION)

    def test_has_v1_to_v3_path(self):
        mgr = create_default_manager()
        path = mgr.get_migration_path(IndexVersion(1, 0, 0))
        self.assertEqual(len(path), 2)

    def test_has_v2_to_v3_path(self):
        mgr = create_default_manager()
        path = mgr.get_migration_path(IndexVersion(2, 0, 0))
        self.assertEqual(len(path), 1)


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