"""
Tests for Dazzle-Back runtime module.
"""

from typing import Any
from uuid import uuid4

import pytest
from pydantic import BaseModel

from dazzle_back.runtime.model_generator import (  # noqa: E402
    generate_all_entity_models,
    generate_create_schema,
    generate_entity_model,
    generate_update_schema,
)
from dazzle_back.runtime.service_generator import (  # noqa: E402
    CRUDService,
    CustomService,
    ServiceFactory,
)
from dazzle_back.specs import (  # noqa: E402
    BackendSpec,
    DomainOperation,
    EndpointSpec,
    EntitySpec,
    FieldSpec,
    FieldType,
    HttpMethod,
    OperationKind,
    ScalarType,
    SchemaFieldSpec,
    SchemaSpec,
    ServiceSpec,
)

# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture
def task_entity() -> EntitySpec:
    """Create a sample Task entity."""
    return EntitySpec(
        name="Task",
        label="Task",
        description="A task to be completed",
        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, max_length=200),
                required=True,
            ),
            FieldSpec(
                name="description",
                type=FieldType(kind="scalar", scalar_type=ScalarType.TEXT),
                required=False,
            ),
            FieldSpec(
                name="status",
                type=FieldType(kind="enum", enum_values=["pending", "in_progress", "done"]),
                required=True,
                default="pending",
            ),
            FieldSpec(
                name="priority",
                type=FieldType(kind="scalar", scalar_type=ScalarType.INT),
                required=False,
                default=1,
            ),
        ],
    )


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


@pytest.fixture
def list_tasks_service() -> ServiceSpec:
    """Create a list tasks service spec."""
    return ServiceSpec(
        name="list_tasks",
        description="List all tasks",
        inputs=SchemaSpec(
            fields=[
                SchemaFieldSpec(name="page", type="int", required=False),
                SchemaFieldSpec(name="page_size", type="int", required=False),
            ]
        ),
        outputs=SchemaSpec(
            fields=[
                SchemaFieldSpec(name="items", type="list[Task]", required=True),
                SchemaFieldSpec(name="total", type="int", required=True),
            ]
        ),
        domain_operation=DomainOperation(kind=OperationKind.LIST, entity="Task"),
    )


# =============================================================================
# Model Generator Tests
# =============================================================================


class TestModelGenerator:
    """Tests for model generation."""

    def test_generate_entity_model(self, task_entity: EntitySpec) -> None:
        """Test generating a Pydantic model from EntitySpec."""
        TaskModel = generate_entity_model(task_entity)

        # Check model name
        assert TaskModel.__name__ == "Task"

        # Check fields exist
        field_names = set(TaskModel.model_fields.keys())
        assert "title" in field_names
        assert "description" in field_names
        assert "status" in field_names
        assert "priority" in field_names
        assert "id" in field_names

    def test_model_instance_creation(self, task_entity: EntitySpec) -> None:
        """Test creating an instance of a generated model."""
        TaskModel = generate_entity_model(task_entity)

        # Create instance with required fields
        task_id = uuid4()
        task = TaskModel(id=task_id, title="Test Task", status="pending")

        assert task.title == "Test Task"  # type: ignore
        assert task.status == "pending"  # type: ignore
        assert task.description is None  # type: ignore

    def test_model_with_defaults(self, task_entity: EntitySpec) -> None:
        """Test that default values are applied."""
        TaskModel = generate_entity_model(task_entity)

        # Create instance - priority should get default value when not provided
        task_id = uuid4()
        task = TaskModel(id=task_id, title="Test2", status="done")
        assert task.priority == 1  # type: ignore

    def test_generate_all_entity_models(
        self, task_entity: EntitySpec, user_entity: EntitySpec
    ) -> None:
        """Test generating multiple models."""
        models = generate_all_entity_models([task_entity, user_entity])

        assert "Task" in models
        assert "User" in models
        assert models["Task"].__name__ == "Task"
        assert models["User"].__name__ == "User"

    def test_generate_create_schema(self, task_entity: EntitySpec) -> None:
        """Test generating a create schema."""
        CreateSchema = generate_create_schema(task_entity)

        # Should not have id field
        field_names = set(CreateSchema.model_fields.keys())
        assert "id" not in field_names
        assert "title" in field_names

    def test_generate_update_schema(self, task_entity: EntitySpec) -> None:
        """Test generating an update schema."""
        UpdateSchema = generate_update_schema(task_entity)

        # All fields should be optional
        field_names = set(UpdateSchema.model_fields.keys())
        assert "id" not in field_names
        assert "title" in field_names

        # Should be able to create with partial data
        update = UpdateSchema(title="New Title")
        assert update.title == "New Title"  # type: ignore


# =============================================================================
# Service Generator Tests
# =============================================================================


class TestServiceGenerator:
    """Tests for service generation."""

    @pytest.mark.asyncio
    async def test_crud_service_create(self, task_entity: EntitySpec) -> None:
        """Test CRUD service create operation."""
        TaskModel = generate_entity_model(task_entity)
        CreateSchema = generate_create_schema(task_entity)

        service: CRUDService[BaseModel, BaseModel, BaseModel] = CRUDService(
            entity_name="Task",
            model_class=TaskModel,
            create_schema=CreateSchema,
            update_schema=CreateSchema,  # Simplified
        )

        # Create a task
        create_data = CreateSchema(title="New Task", status="pending")
        task = await service.create(create_data)

        assert task.title == "New Task"  # type: ignore
        assert task.status == "pending"  # type: ignore
        assert task.id is not None  # type: ignore

    @pytest.mark.asyncio
    async def test_crud_service_read(self, task_entity: EntitySpec) -> None:
        """Test CRUD service read operation."""
        TaskModel = generate_entity_model(task_entity)
        CreateSchema = generate_create_schema(task_entity)

        service: CRUDService[BaseModel, BaseModel, BaseModel] = CRUDService(
            entity_name="Task",
            model_class=TaskModel,
            create_schema=CreateSchema,
            update_schema=CreateSchema,
        )

        # Create and then read
        create_data = CreateSchema(title="Test Task", status="pending")
        created = await service.create(create_data)

        task = await service.read(created.id)  # type: ignore

        assert task is not None
        assert task.title == "Test Task"  # type: ignore

    @pytest.mark.asyncio
    async def test_crud_service_update(self, task_entity: EntitySpec) -> None:
        """Test CRUD service update operation."""
        TaskModel = generate_entity_model(task_entity)
        CreateSchema = generate_create_schema(task_entity)
        UpdateSchema = generate_update_schema(task_entity)

        service: CRUDService[BaseModel, BaseModel, BaseModel] = CRUDService(
            entity_name="Task",
            model_class=TaskModel,
            create_schema=CreateSchema,
            update_schema=UpdateSchema,
        )

        # Create a task
        create_data = CreateSchema(title="Original", status="pending")
        created = await service.create(create_data)

        # Update it
        update_data = UpdateSchema(title="Updated")
        updated = await service.update(created.id, update_data)  # type: ignore

        assert updated is not None
        assert updated.title == "Updated"  # type: ignore
        assert updated.status == "pending"  # type: ignore  # Unchanged

    @pytest.mark.asyncio
    async def test_crud_service_delete(self, task_entity: EntitySpec) -> None:
        """Test CRUD service delete operation."""
        TaskModel = generate_entity_model(task_entity)
        CreateSchema = generate_create_schema(task_entity)

        service: CRUDService[BaseModel, BaseModel, BaseModel] = CRUDService(
            entity_name="Task",
            model_class=TaskModel,
            create_schema=CreateSchema,
            update_schema=CreateSchema,
        )

        # Create and delete
        create_data = CreateSchema(title="To Delete", status="pending")
        created = await service.create(create_data)

        result = await service.delete(created.id)  # type: ignore
        assert result is True

        # Verify deleted
        task = await service.read(created.id)  # type: ignore
        assert task is None

    @pytest.mark.asyncio
    async def test_crud_service_list(self, task_entity: EntitySpec) -> None:
        """Test CRUD service list operation."""
        TaskModel = generate_entity_model(task_entity)
        CreateSchema = generate_create_schema(task_entity)

        service: CRUDService[BaseModel, BaseModel, BaseModel] = CRUDService(
            entity_name="Task",
            model_class=TaskModel,
            create_schema=CreateSchema,
            update_schema=CreateSchema,
        )

        # Create multiple tasks
        for i in range(5):
            await service.create(CreateSchema(title=f"Task {i}", status="pending"))

        # List with pagination
        result = await service.list(page=1, page_size=3)

        assert result["total"] == 5
        assert len(result["items"]) == 3
        assert result["page"] == 1
        assert result["page_size"] == 3

    @pytest.mark.asyncio
    async def test_custom_service(self) -> None:
        """Test custom service execution."""
        service: Any = CustomService(service_name="calculate_total")

        result = await service.execute()

        assert result["status"] == "ok"
        assert result["service"] == "calculate_total"

    @pytest.mark.asyncio
    async def test_custom_service_with_handler(self) -> None:
        """Test custom service with a handler."""
        service: Any = CustomService(service_name="add_numbers")

        async def add_handler(a: int, b: int) -> dict[str, int]:
            return {"sum": a + b}

        service.set_handler(add_handler)

        result = await service.execute(a=5, b=3)
        assert result["sum"] == 8

    def test_service_factory(
        self, task_entity: EntitySpec, list_tasks_service: ServiceSpec
    ) -> None:
        """Test service factory creates appropriate services."""
        models = generate_all_entity_models([task_entity])
        factory = ServiceFactory(models)

        service = factory.create_service(list_tasks_service)

        assert service is not None
        assert factory.get_service("list_tasks") is service


# =============================================================================
# Integration Tests
# =============================================================================


class TestRuntimeIntegration:
    """Integration tests for the complete runtime."""

    def test_backend_spec_to_models(self) -> None:
        """Test converting a complete BackendSpec to models."""
        spec = BackendSpec(
            name="test_app",
            version="1.0.0",
            entities=[
                EntitySpec(
                    name="Item",
                    fields=[
                        FieldSpec(
                            name="name",
                            type=FieldType(kind="scalar", scalar_type=ScalarType.STR),
                            required=True,
                        ),
                    ],
                ),
            ],
            services=[
                ServiceSpec(
                    name="list_items",
                    domain_operation=DomainOperation(kind=OperationKind.LIST, entity="Item"),
                ),
            ],
            endpoints=[
                EndpointSpec(
                    name="list_items_endpoint",
                    service="list_items",
                    method=HttpMethod.GET,
                    path="/items",
                ),
            ],
        )

        # Generate models
        models = generate_all_entity_models(spec.entities)
        assert "Item" in models

        # Create services
        factory = ServiceFactory(models)
        services = factory.create_all_services(spec.services)
        assert "list_items" in services


# =============================================================================
# Server Integration Tests (with SQLite)
# =============================================================================

# Check if FastAPI is available
try:
    import fastapi  # noqa: F401

    FASTAPI_AVAILABLE = True
except ImportError:
    FASTAPI_AVAILABLE = False


@pytest.mark.skipif(not FASTAPI_AVAILABLE, reason="FastAPI not installed")
@pytest.mark.skipif(
    not __import__("os").environ.get("DATABASE_URL"),
    reason="DATABASE_URL not set",
)
class TestServerWithDatabase:
    """Integration tests for server with PostgreSQL persistence."""

    @pytest.fixture
    def simple_appspec(self) -> Any:
        """Create a simple AppSpec for testing."""
        from dazzle.core.ir.appspec import AppSpec
        from dazzle.core.ir.domain import DomainSpec
        from dazzle.core.ir.domain import EntitySpec as IREntitySpec
        from dazzle.core.ir.fields import FieldModifier, FieldTypeKind
        from dazzle.core.ir.fields import FieldSpec as IRFieldSpec
        from dazzle.core.ir.fields import FieldType as IRFieldType

        task_entity = IREntitySpec(
            name="Task",
            title="Task",
            fields=[
                IRFieldSpec(
                    name="id",
                    type=IRFieldType(kind=FieldTypeKind.UUID),
                    modifiers=[FieldModifier.PK],
                ),
                IRFieldSpec(
                    name="title",
                    type=IRFieldType(kind=FieldTypeKind.STR, max_length=200),
                    modifiers=[FieldModifier.REQUIRED],
                ),
                IRFieldSpec(
                    name="status",
                    type=IRFieldType(kind=FieldTypeKind.ENUM, enum_values=["pending", "done"]),
                    modifiers=[FieldModifier.REQUIRED],
                    default="pending",
                ),
            ],
        )

        from dazzle.core.ir.surfaces import SurfaceMode, SurfaceSpec

        task_list_surface = SurfaceSpec(
            name="task_list",
            title="Tasks",
            entity_ref="Task",
            mode=SurfaceMode.LIST,
        )

        return AppSpec(
            name="test_app",
            version="1.0.0",
            domain=DomainSpec(entities=[task_entity]),
            surfaces=[task_list_surface],
        )

    def test_server_creates_database(self, simple_appspec: Any) -> None:
        """Test that server creates database manager with PostgreSQL."""
        import os

        from dazzle_back.runtime.server import DazzleBackendApp

        database_url = os.environ["DATABASE_URL"]
        app_builder = DazzleBackendApp(
            simple_appspec,
            database_url=database_url,
        )
        app_builder.build()

        # Check for database manager
        assert app_builder._db_manager is not None

    def test_server_without_database_raises(self, simple_appspec: Any) -> None:
        """Test that server raises ValueError without database_url."""
        import os

        from dazzle_back.runtime.server import DazzleBackendApp

        # Temporarily clear DATABASE_URL so the builder doesn't pick it up
        saved = os.environ.pop("DATABASE_URL", None)
        try:
            app_builder = DazzleBackendApp(simple_appspec)
            with pytest.raises(ValueError, match="database_url is required"):
                app_builder.build()
        finally:
            if saved is not None:
                os.environ["DATABASE_URL"] = saved

    def test_crud_service_with_repository(self, simple_appspec: Any) -> None:
        """Test that CRUD service is wired up to repository."""
        import os

        from dazzle_back.runtime.server import DazzleBackendApp

        database_url = os.environ["DATABASE_URL"]
        app_builder = DazzleBackendApp(
            simple_appspec,
            database_url=database_url,
        )
        app_builder.build()

        # Get the task list service (named by surface converter convention)
        task_service = app_builder.get_service("list_tasks")
        assert task_service is not None
        assert isinstance(task_service, CRUDService)

        # Repository should be set
        assert task_service._repository is not None

    @pytest.mark.asyncio
    async def test_crud_operations_persist(self, simple_appspec: Any) -> None:
        """Test that CRUD operations persist data to PostgreSQL."""
        import os

        from dazzle_back.runtime.server import DazzleBackendApp

        database_url = os.environ["DATABASE_URL"]
        app_builder = DazzleBackendApp(
            simple_appspec,
            database_url=database_url,
        )
        app_builder.build()

        task_service = app_builder.get_service("list_tasks")
        assert isinstance(task_service, CRUDService)

        # Create schema for test
        from dazzle_back.converters.entity_converter import convert_entities

        backend_entities = convert_entities(simple_appspec.domain.entities)
        CreateSchema = generate_create_schema(backend_entities[0])

        # Create a task
        create_data = CreateSchema(title="Test Task", status="pending")
        task = await task_service.create(create_data)

        task_id = task.id

        # Read it back
        read_task = await task_service.read(task_id)
        assert read_task is not None
        assert read_task.title == "Test Task"  # type: ignore[union-attr]

        # Create a new app instance to verify persistence
        app_builder2 = DazzleBackendApp(
            simple_appspec,
            database_url=database_url,
        )
        app_builder2.build()

        task_service2 = app_builder2.get_service("list_tasks")
        assert task_service2 is not None

        # Should still be able to read the task
        persisted_task = await task_service2.read(task_id)
        assert persisted_task is not None
        assert persisted_task.title == "Test Task"
