"""Unit tests for schema models and element type validation."""

from __future__ import annotations

import pytest
from pydantic import ValidationError

from extractor.schema import (
    ATOMIC_TYPES,
    ELEMENT_TYPES,
    SCHEMA_VERSION,
    BaseElement,
    chunk_id,
    element_id,
    list_id,
)


def make_element(**kwargs) -> dict:
    """Build a minimal valid element dict."""
    defaults = {
        "id": "el_abcdef1234567890",
        "element_type": "text:narrative",
        "text": "Hello world.",
        "section_path": ["Introduction"],
        "section_path_tier": 1,
        "sequence_index": 0,
        "extraction_method": "test",
        "schema_version": SCHEMA_VERSION,
    }
    defaults.update(kwargs)
    return defaults


class TestElementTypeEnum:
    def test_all_families_present(self):
        families = {et.split(":")[0] for et in ELEMENT_TYPES}
        assert "structural" in families
        assert "text" in families
        assert "table" in families
        assert "code" in families
        assert "media" in families
        assert "scientific" in families
        assert "list" in families
        assert "meta" in families
        assert "composite" in families

    def test_canonical_table_names(self):
        assert "table:simple" in ELEMENT_TYPES
        assert "table:complex" in ELEMENT_TYPES
        assert "table:continuation" in ELEMENT_TYPES
        # These wrong names must NOT be in the enum
        assert "table:table" not in ELEMENT_TYPES

    def test_canonical_code_names(self):
        assert "code:block" in ELEMENT_TYPES
        assert "code:inline" in ELEMENT_TYPES
        # Wrong name must not exist
        assert "code:code_block" not in ELEMENT_TYPES

    def test_canonical_equation_names(self):
        assert "scientific:equation_display" in ELEMENT_TYPES
        assert "scientific:equation_inline" in ELEMENT_TYPES
        # Wrong names must not exist
        assert "equation:block" not in ELEMENT_TYPES
        assert "equation:inline" not in ELEMENT_TYPES

    def test_admonition_types_present(self):
        assert "text:admonition" in ELEMENT_TYPES
        assert "text:sidebar" in ELEMENT_TYPES

    def test_transcript_type_present(self):
        assert "text:transcript_segment" in ELEMENT_TYPES

    def test_media_image_present(self):
        assert "media:image" in ELEMENT_TYPES
        assert "media:figure" in ELEMENT_TYPES


class TestAtomicTypes:
    def test_tables_are_atomic(self):
        assert "table:simple" in ATOMIC_TYPES
        assert "table:complex" in ATOMIC_TYPES
        assert "table:continuation" in ATOMIC_TYPES

    def test_code_block_is_atomic(self):
        assert "code:block" in ATOMIC_TYPES

    def test_display_equation_is_atomic(self):
        assert "scientific:equation_display" in ATOMIC_TYPES

    def test_inline_equation_is_not_atomic(self):
        # Inline equations flow with surrounding text
        assert "scientific:equation_inline" not in ATOMIC_TYPES

    def test_media_image_is_atomic(self):
        assert "media:image" in ATOMIC_TYPES

    def test_narrative_is_not_atomic(self):
        assert "text:narrative" not in ATOMIC_TYPES


class TestBaseElementValidation:
    def test_valid_element_parses(self):
        el = BaseElement.model_validate(make_element())
        assert el.element_type == "text:narrative"
        assert el.schema_version == SCHEMA_VERSION

    def test_invalid_element_type_rejected(self):
        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(element_type="table:table"))

    def test_wrong_schema_version_rejected(self):
        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(schema_version="0.9.0"))

    def test_section_path_tier_range(self):
        # Valid range 1-4
        for tier in [1, 2, 3, 4]:
            el = BaseElement.model_validate(make_element(section_path_tier=tier))
            assert el.section_path_tier == tier

        # Out of range rejected
        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(section_path_tier=0))
        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(section_path_tier=5))

    def test_confidence_range(self):
        el = BaseElement.model_validate(make_element(confidence=0.95))
        assert el.confidence == 0.95

        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(confidence=1.5))
        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(confidence=-0.1))

    def test_null_confidence_allowed(self):
        el = BaseElement.model_validate(make_element(confidence=None))
        assert el.confidence is None

    def test_sequence_index_non_negative(self):
        with pytest.raises(ValidationError):
            BaseElement.model_validate(make_element(sequence_index=-1))

    def test_table_dual_format(self):
        table_el = make_element(
            element_type="table:simple",
            table={
                "markdown": "| A | B |\n|---|---|\n| 1 | 2 |",
                "structured": {"headers": ["A", "B"], "rows": [["1", "2"]]},
                "has_header_row": True,
                "row_count": 1,
                "header_row_count": 1,
                "col_count": 2,
            },
        )
        el = BaseElement.model_validate(table_el)
        assert el.table is not None
        assert el.table.markdown == "| A | B |\n|---|---|\n| 1 | 2 |"
        assert el.table.structured.headers == ["A", "B"]
        assert el.table.structured.rows == [["1", "2"]]


class TestIDDerivation:
    def test_element_id_format(self):
        eid = element_id("hello", 1, 0, "abc123")
        assert eid.startswith("el_")
        assert len(eid) == 3 + 16  # "el_" + 16 hex chars

    def test_element_id_deterministic(self):
        eid1 = element_id("hello", 1, 0, "abc123")
        eid2 = element_id("hello", 1, 0, "abc123")
        assert eid1 == eid2

    def test_element_id_differs_on_text(self):
        eid1 = element_id("hello", 1, 0, "abc123")
        eid2 = element_id("world", 1, 0, "abc123")
        assert eid1 != eid2

    def test_element_id_differs_on_page(self):
        eid1 = element_id("hello", 1, 0, "abc123")
        eid2 = element_id("hello", 2, 0, "abc123")
        assert eid1 != eid2

    def test_element_id_differs_on_source(self):
        eid1 = element_id("hello", 1, 0, "source_a")
        eid2 = element_id("hello", 1, 0, "source_b")
        assert eid1 != eid2

    def test_chunk_id_format(self):
        cid = chunk_id(["el_abc", "el_def"], "sha256", 0)
        assert cid.startswith("ch_")
        assert len(cid) == 3 + 16

    def test_list_id_format(self):
        lid = list_id("sha256abc", 42)
        assert lid.startswith("li_")
        assert len(lid) == 3 + 16

    def test_no_collisions_in_document(self):
        """No ID collisions in a 1000-element synthetic document."""
        sha = "e3b0c44298fc1c149afb"
        ids = [element_id(f"text chunk number {i}", i // 10, i, sha) for i in range(1000)]
        assert len(set(ids)) == 1000, "ID collision detected in 1000-element document"
