"""Tests for vector index schema — value objects and OpenSearch mapping."""

from __future__ import annotations

import pytest

from domain.vector_schema import (
    DEFAULT_EMBEDDING_DIMENSION,
    DEFAULT_INDEX_NAME,
    ChunkBboxEntry,
    ChunkOrigin,
    DocItemRef,
    IndexedChunk,
    SearchResult,
    build_index_mapping,
)


class TestChunkBboxEntry:
    def test_construction(self):
        bbox = ChunkBboxEntry(page=1, x=10.0, y=20.0, w=100.0, h=50.0)
        assert bbox.page == 1
        assert bbox.x == 10.0
        assert bbox.w == 100.0

    def test_frozen(self):
        bbox = ChunkBboxEntry(page=1, x=0, y=0, w=10, h=10)
        with pytest.raises(AttributeError):
            bbox.page = 2  # type: ignore[misc]


class TestDocItemRef:
    def test_construction(self):
        ref = DocItemRef(self_ref="#/texts/0", label="text")
        assert ref.self_ref == "#/texts/0"
        assert ref.label == "text"


class TestChunkOrigin:
    def test_construction(self):
        origin = ChunkOrigin(binary_hash="abc123", filename="doc.pdf")
        assert origin.binary_hash == "abc123"
        assert origin.filename == "doc.pdf"


class TestIndexedChunk:
    def _make_chunk(self, **overrides) -> IndexedChunk:
        defaults = {
            "doc_id": "doc-1",
            "filename": "test.pdf",
            "content": "Hello world",
            "embedding": [0.1] * 384,
            "chunk_index": 0,
            "chunk_type": "text",
            "page_number": 1,
        }
        defaults.update(overrides)
        return IndexedChunk(**defaults)

    def test_minimal_chunk(self):
        chunk = self._make_chunk()
        assert chunk.doc_id == "doc-1"
        assert chunk.content == "Hello world"
        assert chunk.bboxes == []
        assert chunk.headings == []
        assert chunk.doc_items == []
        assert chunk.origin is None

    def test_full_chunk(self):
        chunk = self._make_chunk(
            bboxes=[ChunkBboxEntry(page=1, x=10, y=20, w=100, h=50)],
            headings=["Chapter 1", "Section A"],
            doc_items=[DocItemRef(self_ref="#/texts/0", label="text")],
            origin=ChunkOrigin(binary_hash="abc", filename="test.pdf"),
        )
        assert len(chunk.bboxes) == 1
        assert chunk.headings == ["Chapter 1", "Section A"]
        assert chunk.doc_items[0].label == "text"
        assert chunk.origin.binary_hash == "abc"

    def test_to_dict_minimal(self):
        chunk = self._make_chunk()
        d = chunk.to_dict()
        assert d["doc_id"] == "doc-1"
        assert d["filename"] == "test.pdf"
        assert d["content"] == "Hello world"
        assert d["embedding"] == [0.1] * 384
        assert d["chunk_index"] == 0
        assert d["chunk_type"] == "text"
        assert d["page_number"] == 1
        assert d["bboxes"] == []
        assert d["headings"] == []
        assert d["doc_items"] == []
        assert "origin" not in d

    def test_to_dict_full(self):
        chunk = self._make_chunk(
            bboxes=[ChunkBboxEntry(page=1, x=10.5, y=20.0, w=100.0, h=50.0)],
            headings=["H1"],
            doc_items=[DocItemRef(self_ref="#/texts/0", label="text")],
            origin=ChunkOrigin(binary_hash="abc", filename="test.pdf"),
        )
        d = chunk.to_dict()
        assert d["bboxes"] == [{"page": 1, "x": 10.5, "y": 20.0, "w": 100.0, "h": 50.0}]
        assert d["headings"] == ["H1"]
        assert d["doc_items"] == [{"self_ref": "#/texts/0", "label": "text"}]
        assert d["origin"] == {"binary_hash": "abc", "filename": "test.pdf"}

    def test_frozen(self):
        chunk = self._make_chunk()
        with pytest.raises(AttributeError):
            chunk.content = "modified"  # type: ignore[misc]


class TestBuildIndexMapping:
    def test_default_dimension(self):
        mapping = build_index_mapping()
        props = mapping["mappings"]["properties"]
        assert props["embedding"]["dimension"] == 384
        assert props["embedding"]["type"] == "knn_vector"
        assert props["embedding"]["method"]["engine"] == "faiss"
        assert props["embedding"]["method"]["name"] == "hnsw"

    def test_custom_dimension(self):
        mapping = build_index_mapping(embedding_dimension=768)
        assert mapping["mappings"]["properties"]["embedding"]["dimension"] == 768

    def test_knn_enabled(self):
        mapping = build_index_mapping()
        assert mapping["settings"]["index"]["knn"] is True

    def test_all_fields_present(self):
        mapping = build_index_mapping()
        props = mapping["mappings"]["properties"]
        expected_fields = {
            "doc_id",
            "filename",
            "content",
            "embedding",
            "chunk_index",
            "chunk_type",
            "page_number",
            "bboxes",
            "headings",
            "doc_items",
            "origin",
        }
        assert set(props.keys()) == expected_fields

    def test_bboxes_nested_type(self):
        mapping = build_index_mapping()
        bboxes = mapping["mappings"]["properties"]["bboxes"]
        assert bboxes["type"] == "nested"
        assert "page" in bboxes["properties"]
        assert "x" in bboxes["properties"]
        assert "y" in bboxes["properties"]
        assert "w" in bboxes["properties"]
        assert "h" in bboxes["properties"]

    def test_doc_items_nested_type(self):
        mapping = build_index_mapping()
        doc_items = mapping["mappings"]["properties"]["doc_items"]
        assert doc_items["type"] == "nested"
        assert "self_ref" in doc_items["properties"]
        assert "label" in doc_items["properties"]

    def test_origin_object_type(self):
        mapping = build_index_mapping()
        origin = mapping["mappings"]["properties"]["origin"]
        assert origin["type"] == "object"
        assert "binary_hash" in origin["properties"]
        assert "filename" in origin["properties"]

    def test_content_uses_standard_analyzer(self):
        mapping = build_index_mapping()
        content = mapping["mappings"]["properties"]["content"]
        assert content["type"] == "text"
        assert content["analyzer"] == "standard"

    def test_keyword_fields(self):
        mapping = build_index_mapping()
        props = mapping["mappings"]["properties"]
        for field_name in ("doc_id", "filename", "chunk_type"):
            assert props[field_name]["type"] == "keyword", f"{field_name} should be keyword"

    def test_integer_fields(self):
        mapping = build_index_mapping()
        props = mapping["mappings"]["properties"]
        for field_name in ("chunk_index", "page_number"):
            assert props[field_name]["type"] == "integer", f"{field_name} should be integer"


class TestSearchResult:
    def test_construction(self):
        chunk = IndexedChunk(
            doc_id="doc-1",
            filename="test.pdf",
            content="Hello",
            embedding=[0.1] * 384,
            chunk_index=0,
            chunk_type="text",
            page_number=1,
        )
        result = SearchResult(chunk=chunk, score=0.95)
        assert result.chunk.content == "Hello"
        assert result.score == 0.95

    def test_frozen(self):
        chunk = IndexedChunk(
            doc_id="d",
            filename="f",
            content="c",
            embedding=[],
            chunk_index=0,
            chunk_type="text",
            page_number=1,
        )
        result = SearchResult(chunk=chunk, score=0.5)
        with pytest.raises(AttributeError):
            result.score = 0.9  # type: ignore[misc]


class TestConstants:
    def test_default_embedding_dimension(self):
        assert DEFAULT_EMBEDDING_DIMENSION == 384

    def test_default_index_name(self):
        assert DEFAULT_INDEX_NAME == "docling-studio-chunks"
