"""Tests for rbsp.runtime.schema -- index schema definitions and validation."""

import json
import os
import sys
import unittest

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

from rbsp.runtime.schema import (
    FieldType,
    FieldConfig,
    Schema,
    default_code_schema,
)


class TestFieldType(unittest.TestCase):
    """Tests for the FieldType enum."""

    def test_all_types_exist(self):
        expected = {"TEXT", "KEYWORD", "INTEGER", "FLOAT", "BOOLEAN", "DATE", "BINARY"}
        self.assertEqual({t.name for t in FieldType}, expected)

    def test_values_are_lowercase(self):
        for t in FieldType:
            self.assertEqual(t.value, t.name.lower())

    def test_construct_from_value(self):
        self.assertIs(FieldType("text"), FieldType.TEXT)
        self.assertIs(FieldType("keyword"), FieldType.KEYWORD)
        self.assertIs(FieldType("integer"), FieldType.INTEGER)


class TestFieldConfig(unittest.TestCase):
    """Tests for the FieldConfig dataclass."""

    def test_defaults(self):
        fc = FieldConfig("title", FieldType.TEXT)
        self.assertEqual(fc.name, "title")
        self.assertIs(fc.field_type, FieldType.TEXT)
        self.assertTrue(fc.indexed)
        self.assertTrue(fc.stored)
        self.assertTrue(fc.analyzed)
        self.assertAlmostEqual(fc.boost, 1.0)
        self.assertFalse(fc.doc_values)

    def test_custom_values(self):
        fc = FieldConfig(
            "tag", FieldType.KEYWORD,
            indexed=True, stored=False, analyzed=False,
            boost=1.5, doc_values=True,
        )
        self.assertFalse(fc.stored)
        self.assertFalse(fc.analyzed)
        self.assertAlmostEqual(fc.boost, 1.5)
        self.assertTrue(fc.doc_values)

    def test_to_dict(self):
        fc = FieldConfig("x", FieldType.INTEGER, indexed=False)
        d = fc.to_dict()
        self.assertEqual(d["name"], "x")
        self.assertEqual(d["field_type"], "integer")
        self.assertFalse(d["indexed"])

    def test_from_dict(self):
        data = {
            "name": "size",
            "field_type": "float",
            "indexed": False,
            "stored": True,
            "analyzed": False,
            "boost": 0.5,
            "doc_values": True,
        }
        fc = FieldConfig.from_dict(data)
        self.assertEqual(fc.name, "size")
        self.assertIs(fc.field_type, FieldType.FLOAT)
        self.assertFalse(fc.indexed)
        self.assertAlmostEqual(fc.boost, 0.5)
        self.assertTrue(fc.doc_values)

    def test_from_dict_defaults(self):
        data = {"name": "x", "field_type": "text"}
        fc = FieldConfig.from_dict(data)
        self.assertTrue(fc.indexed)
        self.assertTrue(fc.stored)
        self.assertTrue(fc.analyzed)
        self.assertAlmostEqual(fc.boost, 1.0)
        self.assertFalse(fc.doc_values)

    def test_roundtrip(self):
        original = FieldConfig("body", FieldType.TEXT, boost=3.0, doc_values=True)
        restored = FieldConfig.from_dict(original.to_dict())
        self.assertEqual(restored.name, original.name)
        self.assertIs(restored.field_type, original.field_type)
        self.assertEqual(restored.boost, original.boost)
        self.assertEqual(restored.doc_values, original.doc_values)


class TestSchema(RBSPTestCase):
    """Tests for the Schema class."""

    def _make_schema(self) -> Schema:
        s = Schema("test")
        s.add_field(FieldConfig("title", FieldType.TEXT, boost=2.0))
        s.add_field(FieldConfig("tag", FieldType.KEYWORD, analyzed=False))
        s.add_field(FieldConfig("count", FieldType.INTEGER, indexed=False))
        return s

    def test_init_default_name(self):
        s = Schema()
        self.assertEqual(s.name, "default")

    def test_init_custom_name(self):
        s = Schema("my_index")
        self.assertEqual(s.name, "my_index")

    def test_add_field(self):
        s = Schema()
        s.add_field(FieldConfig("x", FieldType.TEXT))
        self.assertIsNotNone(s.get_field("x"))

    def test_add_duplicate_field_raises(self):
        s = Schema()
        s.add_field(FieldConfig("x", FieldType.TEXT))
        with self.assertRaises(ValueError):
            s.add_field(FieldConfig("x", FieldType.KEYWORD))

    def test_get_field_existing(self):
        s = self._make_schema()
        f = s.get_field("title")
        self.assertIsNotNone(f)
        self.assertEqual(f.name, "title")
        self.assertIs(f.field_type, FieldType.TEXT)

    def test_get_field_missing(self):
        s = self._make_schema()
        self.assertIsNone(s.get_field("nonexistent"))

    def test_fields_property(self):
        s = self._make_schema()
        self.assertEqual(len(s.fields), 3)

    def test_text_fields(self):
        s = self._make_schema()
        tf = s.text_fields()
        self.assertEqual(len(tf), 1)
        self.assertEqual(tf[0].name, "title")

    def test_keyword_fields(self):
        s = self._make_schema()
        kf = s.keyword_fields()
        self.assertEqual(len(kf), 1)
        self.assertEqual(kf[0].name, "tag")

    def test_indexed_fields(self):
        s = self._make_schema()
        indexed = s.indexed_fields()
        names = {f.name for f in indexed}
        self.assertIn("title", names)
        self.assertIn("tag", names)
        self.assertNotIn("count", names)

    def test_stored_fields(self):
        s = Schema()
        s.add_field(FieldConfig("a", FieldType.TEXT, stored=True))
        s.add_field(FieldConfig("b", FieldType.TEXT, stored=False))
        stored = s.stored_fields()
        self.assertEqual(len(stored), 1)
        self.assertEqual(stored[0].name, "a")

    def test_repr(self):
        s = self._make_schema()
        r = repr(s)
        self.assertIn("test", r)
        self.assertIn("3", r)


class TestSchemaValidation(RBSPTestCase):
    """Tests for Schema.validate_document()."""

    def _make_schema(self) -> Schema:
        s = Schema("test")
        s.add_field(FieldConfig("title", FieldType.TEXT))
        s.add_field(FieldConfig("count", FieldType.INTEGER))
        s.add_field(FieldConfig("score", FieldType.FLOAT))
        s.add_field(FieldConfig("active", FieldType.BOOLEAN))
        s.add_field(FieldConfig("tag", FieldType.KEYWORD))
        return s

    def test_valid_document(self):
        s = self._make_schema()
        errors = s.validate_document({
            "title": "hello",
            "count": 42,
            "score": 3.14,
            "active": True,
            "tag": "python",
        })
        self.assertEqual(errors, [])

    def test_missing_fields_ok(self):
        s = self._make_schema()
        errors = s.validate_document({"title": "hello"})
        self.assertEqual(errors, [])

    def test_none_values_ok(self):
        s = self._make_schema()
        errors = s.validate_document({"title": None, "count": None})
        self.assertEqual(errors, [])

    def test_wrong_type_text(self):
        s = self._make_schema()
        errors = s.validate_document({"title": 123})
        self.assertEqual(len(errors), 1)
        self.assertIn("title", errors[0])

    def test_wrong_type_integer(self):
        s = self._make_schema()
        errors = s.validate_document({"count": "not a number"})
        self.assertEqual(len(errors), 1)
        self.assertIn("count", errors[0])

    def test_wrong_type_boolean(self):
        s = self._make_schema()
        errors = s.validate_document({"active": "yes"})
        self.assertEqual(len(errors), 1)
        self.assertIn("active", errors[0])

    def test_float_accepts_int(self):
        s = self._make_schema()
        errors = s.validate_document({"score": 10})
        self.assertEqual(errors, [])

    def test_unknown_field(self):
        s = self._make_schema()
        errors = s.validate_document({"title": "ok", "mystery": 99})
        self.assertEqual(len(errors), 1)
        self.assertIn("mystery", errors[0])

    def test_multiple_errors(self):
        s = self._make_schema()
        errors = s.validate_document({
            "title": 123,
            "count": "bad",
            "unknown_field": True,
        })
        self.assertEqual(len(errors), 3)

    def test_empty_document(self):
        s = self._make_schema()
        errors = s.validate_document({})
        self.assertEqual(errors, [])


class TestSchemaSerialization(RBSPTestCase):
    """Tests for Schema to_dict / from_dict / save_json / load_json."""

    def _make_schema(self) -> Schema:
        s = Schema("serial_test")
        s.add_field(FieldConfig("path", FieldType.KEYWORD, analyzed=False))
        s.add_field(FieldConfig("body", FieldType.TEXT, boost=1.5))
        s.add_field(FieldConfig("size", FieldType.INTEGER, indexed=False))
        return s

    def test_to_dict(self):
        s = self._make_schema()
        d = s.to_dict()
        self.assertEqual(d["name"], "serial_test")
        self.assertEqual(len(d["fields"]), 3)
        self.assertEqual(d["fields"][0]["name"], "path")

    def test_from_dict(self):
        s = self._make_schema()
        restored = Schema.from_dict(s.to_dict())
        self.assertEqual(restored.name, "serial_test")
        self.assertEqual(len(restored.fields), 3)
        self.assertIsNotNone(restored.get_field("path"))

    def test_roundtrip_dict(self):
        original = self._make_schema()
        restored = Schema.from_dict(original.to_dict())
        self.assertEqual(original.to_dict(), restored.to_dict())

    def test_save_json(self):
        s = self._make_schema()
        path = os.path.join(self.tmp_dir, "schema.json")
        result = s.save_json(path)
        self.assertTrue(result.is_file())
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        self.assertEqual(data["name"], "serial_test")

    def test_load_json(self):
        s = self._make_schema()
        path = os.path.join(self.tmp_dir, "schema.json")
        s.save_json(path)
        loaded = Schema.load_json(path)
        self.assertEqual(loaded.name, "serial_test")
        self.assertEqual(len(loaded.fields), 3)

    def test_roundtrip_json(self):
        original = self._make_schema()
        path = os.path.join(self.tmp_dir, "schema.json")
        original.save_json(path)
        loaded = Schema.load_json(path)
        self.assertEqual(original.to_dict(), loaded.to_dict())

    def test_save_json_creates_parent_dirs(self):
        s = self._make_schema()
        path = os.path.join(self.tmp_dir, "deep", "nested", "schema.json")
        result = s.save_json(path)
        self.assertTrue(result.is_file())

    def test_from_dict_missing_name(self):
        data = {"fields": [{"name": "x", "field_type": "text"}]}
        s = Schema.from_dict(data)
        self.assertEqual(s.name, "default")

    def test_from_dict_empty_fields(self):
        data = {"name": "empty", "fields": []}
        s = Schema.from_dict(data)
        self.assertEqual(len(s.fields), 0)


class TestDefaultCodeSchema(unittest.TestCase):
    """Tests for the default_code_schema() factory."""

    def test_returns_schema(self):
        s = default_code_schema()
        self.assertIsInstance(s, Schema)

    def test_name(self):
        s = default_code_schema()
        self.assertEqual(s.name, "code")

    def test_has_expected_fields(self):
        s = default_code_schema()
        expected = {"path", "filename", "content", "language", "size", "modified"}
        actual = {f.name for f in s.fields}
        self.assertEqual(actual, expected)

    def test_content_is_text(self):
        s = default_code_schema()
        f = s.get_field("content")
        self.assertIs(f.field_type, FieldType.TEXT)
        self.assertTrue(f.analyzed)

    def test_content_boosted(self):
        s = default_code_schema()
        f = s.get_field("content")
        self.assertGreater(f.boost, 1.0)

    def test_path_is_keyword(self):
        s = default_code_schema()
        f = s.get_field("path")
        self.assertIs(f.field_type, FieldType.KEYWORD)
        self.assertFalse(f.analyzed)

    def test_size_is_integer(self):
        s = default_code_schema()
        f = s.get_field("size")
        self.assertIs(f.field_type, FieldType.INTEGER)
        self.assertFalse(f.indexed)

    def test_modified_is_date(self):
        s = default_code_schema()
        f = s.get_field("modified")
        self.assertIs(f.field_type, FieldType.DATE)
        self.assertTrue(f.doc_values)

    def test_language_is_keyword(self):
        s = default_code_schema()
        f = s.get_field("language")
        self.assertIs(f.field_type, FieldType.KEYWORD)
        self.assertFalse(f.analyzed)

    def test_validates_valid_code_doc(self):
        s = default_code_schema()
        doc = {
            "path": "/src/main.py",
            "filename": "main.py",
            "content": "print('hello')",
            "language": "python",
            "size": 1234,
            "modified": 1700000000.0,
        }
        errors = s.validate_document(doc)
        self.assertEqual(errors, [])

    def test_validates_invalid_code_doc(self):
        s = default_code_schema()
        doc = {
            "path": 123,
            "size": "not a number",
        }
        errors = s.validate_document(doc)
        self.assertEqual(len(errors), 2)


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