"""Tests for db_core.schema.es_builder — pure dict generation.

No Elasticsearch connection needed — all tests assert against expected dicts
and JSON structures.  Mirrors the pattern of ``test_schema_builder.py``.
"""

from __future__ import annotations

import sys
from pathlib import Path

parent_dir = Path(__file__).parent.parent.parent
if str(parent_dir) not in sys.path:
    sys.path.insert(0, str(parent_dir))

from db_core.schema.es_builder import (
    ESMappingBuilder,
    ESFieldDef,
    ESIndexDef,
    ESIndexTemplateDef,
)


# ---------------------------------------------------------------------------
# ESFieldDef.to_dict
# ---------------------------------------------------------------------------


def test_es_field_def_to_dict_scalar():
    """A simple keyword field — minimal output."""
    f = ESFieldDef("id", "keyword")
    d = f.to_dict()
    assert d == {"type": "keyword"}


def test_es_field_def_to_dict_text_with_keyword():
    """Text field with a .keyword multi-field."""
    f = ESFieldDef("message", "text", fields={"keyword": {"type": "keyword"}})
    d = f.to_dict()
    assert d == {
        "type": "text",
        "fields": {"keyword": {"type": "keyword"}},
    }


def test_es_field_def_to_dict_with_analyzer():
    """Text field with a custom analyzer."""
    f = ESFieldDef("body", "text", analyzer="english")
    d = f.to_dict()
    assert d["type"] == "text"
    assert d["analyzer"] == "english"
    assert "fields" not in d  # no auto-keyword when not specified


def test_es_field_def_to_dict_date_with_format():
    """Date field with an explicit format string."""
    f = ESFieldDef("created_at", "date", format_="epoch_millis||strict_date_time")
    d = f.to_dict()
    assert d == {
        "type": "date",
        "format": "epoch_millis||strict_date_time",
    }


def test_es_field_def_to_dict_nested():
    """Nested field with recursive sub-properties."""
    f = ESFieldDef(
        "replies",
        "nested",
        properties=[
            ESFieldDef("user_id", "keyword"),
            ESFieldDef("text", "text", fields={"keyword": {"type": "keyword"}}),
        ],
    )
    d = f.to_dict()
    assert d["type"] == "nested"
    assert "properties" in d
    assert d["properties"]["user_id"] == {"type": "keyword"}
    assert d["properties"]["text"] == {
        "type": "text",
        "fields": {"keyword": {"type": "keyword"}},
    }


def test_es_field_def_to_dict_object_enabled():
    """Object field with enabled=False."""
    f = ESFieldDef("metadata", "object", enabled=False)
    d = f.to_dict()
    assert d == {"type": "object", "enabled": False}


def test_es_field_def_to_dict_copy_to():
    """Field that copies its value to another field."""
    f = ESFieldDef("first_name", "text", copy_to="full_name")
    d = f.to_dict()
    assert d["copy_to"] == "full_name"


def test_es_field_def_to_dict_doc_values_false():
    """Field with doc_values disabled."""
    f = ESFieldDef("raw_text", "text", doc_values=False)
    d = f.to_dict()
    assert d["doc_values"] is False


def test_es_field_def_to_dict_index_false():
    """Field that is stored but not indexed."""
    f = ESFieldDef("internal_note", "text", index=False)
    d = f.to_dict()
    assert d["index"] is False


def test_es_field_def_to_dict_omits_none_fields():
    """Fields with None values are omitted from the dict."""
    f = ESFieldDef("name", "text")
    d = f.to_dict()
    # Only "type" should be present
    assert set(d.keys()) == {"type"}


# ---------------------------------------------------------------------------
# ESIndexDef
# ---------------------------------------------------------------------------


def test_es_index_def_basic():
    """Minimal index definition."""
    idx = ESIndexDef(
        name="users",
        properties=[ESFieldDef("id", "keyword")],
    )
    assert idx.name == "users"
    assert len(idx.properties) == 1
    assert idx.settings is None


def test_es_index_def_with_settings():
    """Index definition with custom settings."""
    idx = ESIndexDef(
        name="logs",
        properties=[ESFieldDef("message", "text")],
        settings={"number_of_shards": 3, "number_of_replicas": 2},
    )
    assert idx.settings == {"number_of_shards": 3, "number_of_replicas": 2}


# ---------------------------------------------------------------------------
# ESMappingBuilder.build_mapping
# ---------------------------------------------------------------------------


def test_build_mapping_single_field():
    """Mapping with a single field."""
    idx = ESIndexDef(
        name="t",
        properties=[ESFieldDef("value", "long")],
    )
    mapping = ESMappingBuilder.build_mapping(idx)
    assert mapping == {"properties": {"value": {"type": "long"}}}


def test_build_mapping_multiple_fields():
    """Mapping with multiple fields of different types."""
    idx = ESIndexDef(
        name="articles",
        properties=[
            ESFieldDef("id", "keyword"),
            ESFieldDef("title", "text", fields={"keyword": {"type": "keyword"}}),
            ESFieldDef("views", "long"),
            ESFieldDef("published_at", "date", format_="epoch_millis"),
        ],
    )
    mapping = ESMappingBuilder.build_mapping(idx)
    assert mapping["properties"]["id"] == {"type": "keyword"}
    assert mapping["properties"]["title"]["fields"] == {"keyword": {"type": "keyword"}}
    assert mapping["properties"]["views"] == {"type": "long"}
    assert mapping["properties"]["published_at"]["format"] == "epoch_millis"
    assert len(mapping["properties"]) == 4


def test_build_mapping_nested():
    """Mapping with a nested field containing sub-properties."""
    idx = ESIndexDef(
        name="messages",
        properties=[
            ESFieldDef("id", "keyword"),
            ESFieldDef(
                "comments",
                "nested",
                properties=[
                    ESFieldDef("author", "keyword"),
                    ESFieldDef("body", "text"),
                    ESFieldDef(
                        "reactions",
                        "nested",
                        properties=[ESFieldDef("emoji", "keyword")],
                    ),
                ],
            ),
        ],
    )
    mapping = ESMappingBuilder.build_mapping(idx)
    # Top-level
    assert mapping["properties"]["id"] == {"type": "keyword"}
    # Nested level 1
    comments = mapping["properties"]["comments"]
    assert comments["type"] == "nested"
    assert comments["properties"]["author"] == {"type": "keyword"}
    assert comments["properties"]["body"] == {"type": "text"}
    # Nested level 2 (double nesting)
    reactions = comments["properties"]["reactions"]
    assert reactions["type"] == "nested"
    assert reactions["properties"]["emoji"] == {"type": "keyword"}


# ---------------------------------------------------------------------------
# ESMappingBuilder.build_settings
# ---------------------------------------------------------------------------


def test_build_settings_with_settings():
    """Settings dict is returned as-is."""
    idx = ESIndexDef(
        name="t",
        properties=[],
        settings={"number_of_shards": 1},
    )
    s = ESMappingBuilder.build_settings(idx)
    assert s == {"number_of_shards": 1}


def test_build_settings_none():
    """None settings returns None."""
    idx = ESIndexDef(name="t", properties=[])
    s = ESMappingBuilder.build_settings(idx)
    assert s is None


def test_build_settings_complex():
    """Complex settings with analysis configuration."""
    settings = {
        "number_of_shards": 1,
        "analysis": {
            "analyzer": {
                "custom_analyzer": {
                    "type": "custom",
                    "tokenizer": "standard",
                    "filter": ["lowercase", "stop"],
                }
            }
        },
    }
    idx = ESIndexDef(name="t", properties=[], settings=settings)
    s = ESMappingBuilder.build_settings(idx)
    assert s == settings
    assert s["analysis"]["analyzer"]["custom_analyzer"]["tokenizer"] == "standard"


# ---------------------------------------------------------------------------
# ESMappingBuilder.build_create_body
# ---------------------------------------------------------------------------


def test_build_create_body_full():
    """Full create body with both settings and mappings."""
    idx = ESIndexDef(
        name="test",
        properties=[
            ESFieldDef("id", "keyword"),
            ESFieldDef("name", "text"),
        ],
        settings={"number_of_shards": 1, "number_of_replicas": 0},
    )
    body = ESMappingBuilder.build_create_body(idx)
    assert "settings" in body
    assert "mappings" in body
    assert body["settings"] == {"number_of_shards": 1, "number_of_replicas": 0}
    assert body["mappings"]["properties"]["id"] == {"type": "keyword"}
    assert body["mappings"]["properties"]["name"] == {"type": "text"}


def test_build_create_body_no_settings():
    """Create body without settings omits the 'settings' key entirely."""
    idx = ESIndexDef(
        name="bare",
        properties=[ESFieldDef("x", "keyword")],
    )
    body = ESMappingBuilder.build_create_body(idx)
    assert "settings" not in body
    assert "mappings" in body
    assert body["mappings"]["properties"]["x"] == {"type": "keyword"}


def test_build_create_body_empty_properties():
    """Create body with no fields is still valid ES (properties: {})."""
    idx = ESIndexDef(name="empty", properties=[])
    body = ESMappingBuilder.build_create_body(idx)
    assert body["mappings"] == {"properties": {}}


# ---------------------------------------------------------------------------
# ESMappingBuilder.build_index_template
# ---------------------------------------------------------------------------


def test_build_index_template_basic():
    """Composable index template with minimal fields."""
    idx = ESIndexDef(
        name="inner",
        properties=[ESFieldDef("timestamp", "date")],
        settings={"number_of_shards": 2},
    )
    tpl = ESIndexTemplateDef(
        name="logs-template",
        index_patterns=["logs-*", "events-*"],
        template=idx,
        priority=50,
    )
    body = ESMappingBuilder.build_index_template(tpl)
    assert body["index_patterns"] == ["logs-*", "events-*"]
    assert body["priority"] == 50
    assert body["template"]["settings"] == {"number_of_shards": 2}
    assert body["template"]["mappings"]["properties"]["timestamp"] == {"type": "date"}
    # Defaults should NOT be present
    assert "version" not in body
    assert "composed_of" not in body
    assert "data_stream" not in body


def test_build_index_template_full():
    """Full index template with version, composed_of, and data_stream."""
    idx = ESIndexDef(
        name="inner",
        properties=[ESFieldDef("msg", "text")],
    )
    tpl = ESIndexTemplateDef(
        name="full-template",
        index_patterns=["data-*"],
        template=idx,
        priority=200,
        version=3,
        composed_of=["common-settings", "common-mappings"],
        data_stream={},
    )
    body = ESMappingBuilder.build_index_template(tpl)
    assert body["version"] == 3
    assert body["composed_of"] == ["common-settings", "common-mappings"]
    assert body["data_stream"] == {}


# ---------------------------------------------------------------------------
# ESMappingBuilder.build_component_template
# ---------------------------------------------------------------------------


def test_build_component_template():
    """Component template body."""
    idx = ESIndexDef(
        name="inner",
        properties=[ESFieldDef("host", "keyword")],
        settings={"number_of_replicas": 1},
    )
    body = ESMappingBuilder.build_component_template("common-core", idx, version=1)
    assert body["template"]["settings"] == {"number_of_replicas": 1}
    assert body["template"]["mappings"]["properties"]["host"] == {"type": "keyword"}
    assert body["version"] == 1


def test_build_component_template_no_version():
    """Component template without version omits the version key."""
    idx = ESIndexDef(
        name="inner",
        properties=[ESFieldDef("x", "long")],
    )
    body = ESMappingBuilder.build_component_template("no-ver", idx)
    assert "version" not in body


# ---------------------------------------------------------------------------
# Frozen dataclass immutability
# ---------------------------------------------------------------------------


def test_es_field_def_is_frozen():
    """ESFieldDef is a frozen dataclass — cannot be mutated."""
    f = ESFieldDef("x", "keyword")
    try:
        f.name = "y"  # type: ignore[misc]
        assert False, "Should have raised FrozenInstanceError"
    except Exception:
        pass  # expected


def test_es_index_def_is_frozen():
    """ESIndexDef is a frozen dataclass."""
    idx = ESIndexDef(name="t", properties=[])
    try:
        idx.name = "other"  # type: ignore[misc]
        assert False, "Should have raised FrozenInstanceError"
    except Exception:
        pass


def test_es_index_template_def_is_frozen():
    """ESIndexTemplateDef is a frozen dataclass."""
    idx = ESIndexDef(name="t", properties=[])
    tpl = ESIndexTemplateDef(name="tmpl", index_patterns=["*"], template=idx)
    try:
        tpl.priority = 999  # type: ignore[misc]
        assert False, "Should have raised FrozenInstanceError"
    except Exception:
        pass
