"""
Tests for relationship handling and nested data loading.

Tests relation registry, relation loader, and foreign key handling.
"""

import sqlite3
import tempfile
from pathlib import Path
from typing import Any
from uuid import uuid4

import pytest

from dazzle_back.runtime.relation_loader import (
    RelationInfo,
    RelationLoader,
    RelationRegistry,
    build_foreign_key_constraint,
    get_foreign_key_constraints,
    get_foreign_key_indexes,
)
from dazzle_back.specs.entity import (
    EntitySpec,
    FieldSpec,
    FieldType,
    OnDeleteAction,
    RelationKind,
    RelationSpec,
    ScalarType,
)

# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def user_entity() -> Any:
    """Create a User entity spec."""
    return EntitySpec(
        name="User",
        fields=[
            FieldSpec(
                name="id",
                type=FieldType(kind="scalar", scalar_type=ScalarType.UUID),
                required=True,
            ),
            FieldSpec(
                name="name",
                type=FieldType(kind="scalar", scalar_type=ScalarType.STR),
                required=True,
            ),
            FieldSpec(
                name="email",
                type=FieldType(kind="scalar", scalar_type=ScalarType.EMAIL),
                required=True,
            ),
        ],
    )


@pytest.fixture
def task_entity() -> Any:
    """Create a Task entity spec with owner relation."""
    return EntitySpec(
        name="Task",
        fields=[
            FieldSpec(
                name="id",
                type=FieldType(kind="scalar", scalar_type=ScalarType.UUID),
                required=True,
            ),
            FieldSpec(
                name="title",
                type=FieldType(kind="scalar", scalar_type=ScalarType.STR),
                required=True,
            ),
            FieldSpec(
                name="owner_id",
                type=FieldType(kind="ref", ref_entity="User"),
                required=True,
            ),
        ],
        relations=[
            RelationSpec(
                name="owner",
                from_entity="Task",
                to_entity="User",
                kind=RelationKind.MANY_TO_ONE,
                on_delete=OnDeleteAction.CASCADE,
            ),
        ],
    )


@pytest.fixture
def comment_entity() -> Any:
    """Create a Comment entity spec."""
    return EntitySpec(
        name="Comment",
        fields=[
            FieldSpec(
                name="id",
                type=FieldType(kind="scalar", scalar_type=ScalarType.UUID),
                required=True,
            ),
            FieldSpec(
                name="text",
                type=FieldType(kind="scalar", scalar_type=ScalarType.TEXT),
                required=True,
            ),
            FieldSpec(
                name="task_id",
                type=FieldType(kind="ref", ref_entity="Task"),
                required=True,
            ),
        ],
    )


@pytest.fixture
def all_entities(user_entity: Any, task_entity: Any, comment_entity: Any) -> Any:
    """Get all test entities."""
    return [user_entity, task_entity, comment_entity]


@pytest.fixture
def test_db() -> Any:
    """Create a test database with tables."""
    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
        db_path = f.name

    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row

    # Create tables
    conn.execute("""
        CREATE TABLE User (
            id TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT NOT NULL
        )
    """)

    conn.execute("""
        CREATE TABLE Task (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            owner_id TEXT NOT NULL,
            FOREIGN KEY (owner_id) REFERENCES User(id)
        )
    """)

    conn.execute("""
        CREATE TABLE Comment (
            id TEXT PRIMARY KEY,
            text TEXT NOT NULL,
            task_id TEXT NOT NULL,
            FOREIGN KEY (task_id) REFERENCES Task(id)
        )
    """)

    # Insert test data
    user_id = str(uuid4())
    task_id = str(uuid4())
    comment_id = str(uuid4())

    conn.execute(
        "INSERT INTO User (id, name, email) VALUES (?, ?, ?)",
        (user_id, "John Doe", "john@example.com"),
    )
    conn.execute(
        "INSERT INTO Task (id, title, owner_id) VALUES (?, ?, ?)",
        (task_id, "Test Task", user_id),
    )
    conn.execute(
        "INSERT INTO Comment (id, text, task_id) VALUES (?, ?, ?)",
        (comment_id, "Test comment", task_id),
    )

    conn.commit()

    yield conn, {"user_id": user_id, "task_id": task_id, "comment_id": comment_id}

    conn.close()
    Path(db_path).unlink(missing_ok=True)


# =============================================================================
# RelationInfo Tests
# =============================================================================


class TestRelationInfo:
    """Tests for RelationInfo."""

    def test_is_to_one_many_to_one(self) -> None:
        """Test is_to_one for many-to-one relation."""
        info = RelationInfo(
            name="owner",
            from_entity="Task",
            to_entity="User",
            kind="many_to_one",
            foreign_key_field="owner_id",
        )

        assert info.is_to_one is True
        assert info.is_to_many is False

    def test_is_to_one_one_to_one(self) -> None:
        """Test is_to_one for one-to-one relation."""
        info = RelationInfo(
            name="profile",
            from_entity="User",
            to_entity="Profile",
            kind="one_to_one",
            foreign_key_field="profile_id",
        )

        assert info.is_to_one is True
        assert info.is_to_many is False

    def test_is_to_many_one_to_many(self) -> None:
        """Test is_to_many for one-to-many relation."""
        info = RelationInfo(
            name="tasks",
            from_entity="User",
            to_entity="Task",
            kind="one_to_many",
            foreign_key_field="owner_id",
        )

        assert info.is_to_many is True
        assert info.is_to_one is False

    def test_is_to_many_many_to_many(self) -> None:
        """Test is_to_many for many-to-many relation."""
        info = RelationInfo(
            name="tags",
            from_entity="Task",
            to_entity="Tag",
            kind="many_to_many",
            foreign_key_field="task_id",
        )

        assert info.is_to_many is True
        assert info.is_to_one is False


# =============================================================================
# RelationRegistry Tests
# =============================================================================


class TestRelationRegistry:
    """Tests for RelationRegistry."""

    def test_register_and_get(self) -> None:
        """Test registering and retrieving relations."""
        registry = RelationRegistry()
        info = RelationInfo(
            name="owner",
            from_entity="Task",
            to_entity="User",
            kind="many_to_one",
            foreign_key_field="owner_id",
        )
        registry.register("Task", info)

        assert registry.has_relation("Task", "owner")
        assert registry.get_relation("Task", "owner") == info

    def test_get_relations(self) -> None:
        """Test getting all relations for an entity."""
        registry = RelationRegistry()
        info1 = RelationInfo(
            name="owner",
            from_entity="Task",
            to_entity="User",
            kind="many_to_one",
            foreign_key_field="owner_id",
        )
        info2 = RelationInfo(
            name="project",
            from_entity="Task",
            to_entity="Project",
            kind="many_to_one",
            foreign_key_field="project_id",
        )
        registry.register("Task", info1)
        registry.register("Task", info2)

        relations = registry.get_relations("Task")
        assert len(relations) == 2

    def test_has_relation_false(self) -> None:
        """Test has_relation returns False for non-existent relation."""
        registry = RelationRegistry()

        assert registry.has_relation("Task", "nonexistent") is False

    def test_from_entities(self, all_entities: Any) -> None:
        """Test building registry from entity specs."""
        registry = RelationRegistry.from_entities(all_entities)

        # Task should have owner relation
        assert registry.has_relation("Task", "owner")
        owner_rel = registry.get_relation("Task", "owner")
        assert owner_rel is not None
        assert owner_rel.to_entity == "User"

        # Comment should have implicit task relation from ref field
        assert registry.has_relation("Comment", "task")


# =============================================================================
# RelationLoader Tests
# =============================================================================


class TestRelationLoader:
    """Tests for RelationLoader."""

    def test_load_to_one_relation(self, all_entities: Any, test_db: Any) -> None:
        """Test loading a to-one relation."""
        conn, ids = test_db
        registry = RelationRegistry.from_entities(all_entities)
        loader = RelationLoader(registry, all_entities, placeholder="?")

        # Create task rows
        rows = [{"id": ids["task_id"], "title": "Test Task", "owner_id": ids["user_id"]}]

        # Load owner relation
        result = loader.load_relations("Task", rows, ["owner"], conn)

        assert len(result) == 1
        assert result[0]["owner"] is not None
        assert result[0]["owner"]["name"] == "John Doe"
        assert result[0]["owner"]["email"] == "john@example.com"

    def test_load_to_one_null_relation(self, all_entities: Any, test_db: Any) -> None:
        """Test loading to-one relation with null FK."""
        conn, ids = test_db
        registry = RelationRegistry.from_entities(all_entities)
        loader = RelationLoader(registry, all_entities, placeholder="?")

        # Create task row with no owner_id
        rows = [{"id": str(uuid4()), "title": "Orphan Task", "owner_id": None}]

        result = loader.load_relations("Task", rows, ["owner"], conn)

        assert len(result) == 1
        assert result[0]["owner"] is None

    def test_load_to_many_relation(self, all_entities: Any, test_db: Any) -> None:
        """Test loading a to-many relation."""
        conn, ids = test_db
        registry = RelationRegistry()

        # Register a one-to-many relation from Task to Comment
        registry.register(
            "Task",
            RelationInfo(
                name="comments",
                from_entity="Task",
                to_entity="Comment",
                kind="one_to_many",
                foreign_key_field="task_id",
            ),
        )

        loader = RelationLoader(registry, all_entities, placeholder="?")

        rows = [{"id": ids["task_id"], "title": "Test Task"}]
        result = loader.load_relations("Task", rows, ["comments"], conn)

        assert len(result) == 1
        assert "comments" in result[0]
        assert len(result[0]["comments"]) == 1
        assert result[0]["comments"][0]["text"] == "Test comment"

    def test_load_to_many_empty(self, all_entities: Any, test_db: Any) -> None:
        """Test loading to-many relation with no related items."""
        conn, ids = test_db
        registry = RelationRegistry()

        # Register comments relation
        registry.register(
            "Task",
            RelationInfo(
                name="comments",
                from_entity="Task",
                to_entity="Comment",
                kind="one_to_many",
                foreign_key_field="task_id",
            ),
        )

        loader = RelationLoader(registry, all_entities, placeholder="?")

        # Create a task with no comments
        new_task_id = str(uuid4())
        conn.execute(
            "INSERT INTO Task (id, title, owner_id) VALUES (?, ?, ?)",
            (new_task_id, "New Task", ids["user_id"]),
        )
        conn.commit()

        rows = [{"id": new_task_id, "title": "New Task"}]
        result = loader.load_relations("Task", rows, ["comments"], conn)

        assert result[0]["comments"] == []

    def test_load_multiple_relations(self, all_entities: Any, test_db: Any) -> None:
        """Test loading multiple relations at once."""
        conn, ids = test_db
        registry = RelationRegistry.from_entities(all_entities)

        # Add comments relation
        registry.register(
            "Task",
            RelationInfo(
                name="comments",
                from_entity="Task",
                to_entity="Comment",
                kind="one_to_many",
                foreign_key_field="task_id",
            ),
        )

        loader = RelationLoader(registry, all_entities, placeholder="?")

        rows = [{"id": ids["task_id"], "title": "Test Task", "owner_id": ids["user_id"]}]
        result = loader.load_relations("Task", rows, ["owner", "comments"], conn)

        assert result[0]["owner"] is not None
        assert result[0]["comments"] is not None

    def test_load_unknown_relation(self, all_entities: Any, test_db: Any) -> None:
        """Test loading unknown relation is ignored."""
        conn, ids = test_db
        registry = RelationRegistry.from_entities(all_entities)
        loader = RelationLoader(registry, all_entities, placeholder="?")

        rows = [{"id": ids["task_id"], "title": "Test Task"}]
        result = loader.load_relations("Task", rows, ["nonexistent"], conn)

        # Should not modify rows
        assert "nonexistent" not in result[0]

    def test_load_empty_rows(self, all_entities: Any, test_db: Any) -> None:
        """Test loading relations on empty rows list."""
        conn, ids = test_db
        registry = RelationRegistry.from_entities(all_entities)
        loader = RelationLoader(registry, all_entities, placeholder="?")

        result = loader.load_relations("Task", [], ["owner"], conn)

        assert result == []

    def test_batch_loading(self, all_entities: Any, test_db: Any) -> None:
        """Test that multiple rows are batch-loaded efficiently."""
        conn, ids = test_db
        registry = RelationRegistry.from_entities(all_entities)
        loader = RelationLoader(registry, all_entities, placeholder="?")

        # Create another task for the same user
        task2_id = str(uuid4())
        conn.execute(
            "INSERT INTO Task (id, title, owner_id) VALUES (?, ?, ?)",
            (task2_id, "Task 2", ids["user_id"]),
        )
        conn.commit()

        rows = [
            {"id": ids["task_id"], "title": "Task 1", "owner_id": ids["user_id"]},
            {"id": task2_id, "title": "Task 2", "owner_id": ids["user_id"]},
        ]

        result = loader.load_relations("Task", rows, ["owner"], conn)

        # Both should have the same owner loaded
        assert result[0]["owner"]["id"] == result[1]["owner"]["id"]
        assert result[0]["owner"]["name"] == "John Doe"


# =============================================================================
# Foreign Key Tests
# =============================================================================


class TestForeignKeyConstraints:
    """Tests for FK constraint generation."""

    def test_build_fk_constraint_cascade(self) -> None:
        """Test FK constraint with CASCADE on delete."""
        info = RelationInfo(
            name="owner",
            from_entity="Task",
            to_entity="User",
            kind="many_to_one",
            foreign_key_field="owner_id",
            on_delete="cascade",
        )

        sql = build_foreign_key_constraint(info, "Task")

        assert 'FOREIGN KEY ("owner_id")' in sql
        assert 'REFERENCES "User"("id")' in sql
        assert "ON DELETE CASCADE" in sql

    def test_build_fk_constraint_restrict(self) -> None:
        """Test FK constraint with RESTRICT on delete."""
        info = RelationInfo(
            name="owner",
            from_entity="Task",
            to_entity="User",
            kind="many_to_one",
            foreign_key_field="owner_id",
            on_delete="restrict",
        )

        sql = build_foreign_key_constraint(info, "Task")

        assert "ON DELETE RESTRICT" in sql

    def test_build_fk_constraint_set_null(self) -> None:
        """Test FK constraint with SET NULL on delete."""
        info = RelationInfo(
            name="owner",
            from_entity="Task",
            to_entity="User",
            kind="many_to_one",
            foreign_key_field="owner_id",
            on_delete="set_null",
        )

        sql = build_foreign_key_constraint(info, "Task")

        assert "ON DELETE SET NULL" in sql

    def test_get_fk_constraints(self, task_entity: Any, all_entities: Any) -> None:
        """Test getting all FK constraints for an entity.

        Task has both an explicit ``owner`` relation (FK inferred as "owner")
        and an implicit relation from the ``owner_id`` ref field (FK = "owner_id"),
        so two constraints are generated.
        """
        registry = RelationRegistry.from_entities(all_entities)
        constraints = get_foreign_key_constraints(task_entity, registry)

        assert len(constraints) == 2
        combined = " ".join(constraints)
        assert "owner_id" in combined
        assert 'REFERENCES "User"("id")' in combined

    def test_get_fk_indexes(self, task_entity: Any, all_entities: Any) -> None:
        """Test getting FK index statements.

        Both the explicit and implicit owner relations produce indexes.
        """
        registry = RelationRegistry.from_entities(all_entities)
        indexes = get_foreign_key_indexes(task_entity, registry)

        assert len(indexes) == 2
        combined = " ".join(indexes)
        assert "CREATE INDEX" in combined
        assert "owner_id" in combined


# =============================================================================
# Edge Cases
# =============================================================================


class TestEdgeCases:
    """Tests for edge cases and error handling."""

    def test_self_referential_relation(self) -> None:
        """Test self-referential relation (e.g., parent task)."""
        entity = EntitySpec(
            name="Task",
            fields=[
                FieldSpec(
                    name="id",
                    type=FieldType(kind="scalar", scalar_type=ScalarType.UUID),
                ),
                FieldSpec(
                    name="parent_id",
                    type=FieldType(kind="ref", ref_entity="Task"),
                ),
            ],
        )

        registry = RelationRegistry.from_entities([entity])

        assert registry.has_relation("Task", "parent")

    def test_multiple_refs_to_same_entity(self) -> None:
        """Test multiple refs to same entity."""
        entity = EntitySpec(
            name="Task",
            fields=[
                FieldSpec(
                    name="id",
                    type=FieldType(kind="scalar", scalar_type=ScalarType.UUID),
                ),
                FieldSpec(
                    name="created_by_id",
                    type=FieldType(kind="ref", ref_entity="User"),
                ),
                FieldSpec(
                    name="assigned_to_id",
                    type=FieldType(kind="ref", ref_entity="User"),
                ),
            ],
        )

        registry = RelationRegistry.from_entities([entity])

        # Should have both relations
        relations = registry.get_relations("Task")
        assert len(relations) == 2

    def test_relation_with_backref(self) -> None:
        """Test relation with backref name."""
        entity = EntitySpec(
            name="Task",
            fields=[
                FieldSpec(
                    name="id",
                    type=FieldType(kind="scalar", scalar_type=ScalarType.UUID),
                ),
            ],
            relations=[
                RelationSpec(
                    name="owner",
                    from_entity="Task",
                    to_entity="User",
                    kind=RelationKind.MANY_TO_ONE,
                    backref="tasks",
                ),
            ],
        )

        registry = RelationRegistry.from_entities([entity])
        rel = registry.get_relation("Task", "owner")
        assert rel is not None
        assert rel.backref == "tasks"
