"""Backend test generator for Prism.

Generates pytest tests, Factory Boy factories, and test fixtures
for backend code.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from prism.generators.base import GeneratedFile, ModelGenerator, create_init_file
from prism.spec.fields import FieldType
from prism.spec.stack import FileStrategy
from prism.utils.case_conversion import pluralize, to_camel_case, to_snake_case

if TYPE_CHECKING:
    from prism.spec.fields import FieldSpec
    from prism.spec.model import ModelSpec


class BackendTestGenerator(ModelGenerator):
    """Generator for backend tests and factories."""

    def __init__(self, *args, **kwargs) -> None:  # type: ignore[no-untyped-def]
        super().__init__(*args, **kwargs)
        backend_base = Path(self.spec.generator.backend_output)
        # Put tests at backend root level (e.g., packages/backend/tests/)
        # not inside src/ (e.g., packages/backend/src/tests/)
        backend_root = backend_base.parent if backend_base.name == "src" else backend_base
        self.tests_path = backend_root / self.spec.generator.tests_path

    def generate_shared_files(self) -> list[GeneratedFile]:
        """Generate shared test fixtures and conftest."""
        files = [
            self._generate_conftest(),
        ]

        if self.spec.testing.generate_factories:
            files.append(self._generate_factories_init())

        return files

    def generate_model_files(self, model: ModelSpec) -> list[GeneratedFile]:
        """Generate tests and factory for a single model."""
        files = []

        if self.spec.testing.generate_factories:
            files.append(self._generate_factory(model))

        if self.spec.testing.generate_unit_tests:
            files.append(self._generate_service_tests(model))

        if self.spec.testing.generate_integration_tests and model.rest.enabled:
            files.append(self._generate_api_tests(model))

        if self.spec.testing.generate_graphql_tests and model.graphql.enabled:
            files.append(self._generate_graphql_tests(model))

        return files

    def generate_index_files(self) -> list[GeneratedFile]:
        """Generate index files for test directories."""
        return []

    def _generate_conftest(self) -> GeneratedFile:
        """Generate pytest conftest.py with shared fixtures."""
        db_type = self.spec.testing.test_database
        project_name = self.get_package_name()

        content = f'''"""Pytest configuration and shared fixtures.

⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
"""

from __future__ import annotations

import asyncio
import os
from typing import AsyncGenerator, Generator

# Set test database URL before any app imports
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:"

import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.pool import StaticPool

from {project_name}.models.base import Base

# Use {db_type} for tests
'''

        if db_type == "sqlite":
            content += '''TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"

@pytest.fixture(scope="session")
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
    """Create event loop for the test session."""
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()


@pytest_asyncio.fixture(scope="session")
async def engine():
    """Create async engine for tests."""
    engine = create_async_engine(
        TEST_DATABASE_URL,
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    yield engine

    await engine.dispose()

'''
        else:  # PostgreSQL
            content += '''# Configure your test database URL
TEST_DATABASE_URL = "postgresql+asyncpg://test:test@localhost:5432/test_db"

@pytest.fixture(scope="session")
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
    """Create event loop for the test session."""
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()


@pytest_asyncio.fixture(scope="session")
async def engine():
    """Create async engine for tests."""
    engine = create_async_engine(TEST_DATABASE_URL)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    yield engine

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)

    await engine.dispose()

'''

        content += f'''
@pytest_asyncio.fixture
async def db(engine) -> AsyncGenerator[AsyncSession, None]:
    """Get a test database session."""
    async_session = async_sessionmaker(
        engine,
        class_=AsyncSession,
        expire_on_commit=False,
    )

    async with async_session() as session:
        yield session
        await session.rollback()


@pytest_asyncio.fixture
async def client(db):
    """Create test client with database session."""
    from httpx import AsyncClient, ASGITransport
    from {project_name}.main import app
    from {project_name}.database import get_db

    async def override_get_db():
        yield db

    app.dependency_overrides[get_db] = override_get_db

    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        yield client

    app.dependency_overrides.clear()
'''

        return GeneratedFile(
            path=self.tests_path / "conftest.py",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description="Test conftest",
        )

    def _generate_factories_init(self) -> GeneratedFile:
        """Generate __init__.py for factories."""
        imports = []
        exports = []

        for model in self.spec.models:
            snake_name = to_snake_case(model.name)
            imports.append(f"from .{snake_name} import {model.name}Factory")
            exports.append(f"{model.name}Factory")

        return create_init_file(
            self.tests_path / "factories",
            imports,
            exports,
            "Test factories using Factory Boy.",
        )

    def _generate_factory(self, model: ModelSpec) -> GeneratedFile:
        """Generate Factory Boy factory for a model."""
        snake_name = to_snake_case(model.name)

        project_name = self.get_package_name()

        # Check if we need random import (for constrained float/decimal fields or JSON arrays)
        needs_random = any(
            (
                f.type in (FieldType.FLOAT, FieldType.DECIMAL)
                and (f.min_value is not None or f.max_value is not None)
            )
            or (
                f.type == FieldType.JSON
                and f.json_item_type
                and f.json_item_type.lower() in ("int", "integer", "float", "number")
            )
            for f in model.fields
        )

        # Check if we need uuid import (for unique UUID fields)
        needs_uuid = any(f.type == FieldType.UUID and f.unique for f in model.fields)

        random_import = "import random\n" if needs_random else ""
        uuid_import = "import uuid\n" if needs_uuid else ""

        content = f'''"""Factory for {model.name} test data.

⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
"""

from __future__ import annotations

{random_import}{uuid_import}import factory
from factory.alchemy import SQLAlchemyModelFactory

from {project_name}.models.{snake_name} import {model.name}


class {model.name}Factory(SQLAlchemyModelFactory):
    """Factory for creating {model.name} test instances."""

    class Meta:
        model = {model.name}
        # Disable auto-commit for async SQLAlchemy - tests call await db.commit()
        sqlalchemy_session_persistence = None

{self._build_factory_fields(model)}


# Convenience function
def create_{snake_name}(**kwargs) -> {model.name}:
    """Create a {model.name} instance."""
    return {model.name}Factory.create(**kwargs)


def build_{snake_name}(**kwargs) -> {model.name}:
    """Build a {model.name} instance without saving."""
    return {model.name}Factory.build(**kwargs)
'''

        return GeneratedFile(
            path=self.tests_path / "factories" / f"{snake_name}.py",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description=f"Factory for {model.name}",
        )

    def _build_factory_fields(self, model: ModelSpec) -> str:
        """Build factory field definitions."""
        lines = []

        for field in model.fields:
            faker = self._get_faker_for_field(field)
            lines.append(f"    {field.name} = {faker}")

        return "\n".join(lines) if lines else "    pass"

    def _get_unique_faker(self, field: FieldSpec) -> str:
        """Get a Sequence-based faker for unique fields to avoid collisions."""
        if field.type == FieldType.STRING:
            name_lower = field.name.lower()
            if "email" in name_lower:
                return "factory.Sequence(lambda n: f'test{n}@example.com')"
            # Check for IP address fields
            elif (
                ("ip" in name_lower and "address" in name_lower)
                or name_lower == "ip"
                or name_lower.endswith("_ip")
            ):
                return "factory.Sequence(lambda n: f'192.168.1.{n % 255}')"
            # For fields with alphanumeric patterns (like subdomain names)
            elif field.pattern and "a-z" in field.pattern and "0-9" in field.pattern:
                return "factory.Sequence(lambda n: f'item{n}')"
            return f"factory.Sequence(lambda n: f'{field.name}{{n}}')"
        elif field.type == FieldType.INTEGER:
            min_val = int(field.min_value) if field.min_value is not None else 1
            return f"factory.Sequence(lambda n: {min_val} + n)"
        elif field.type == FieldType.UUID:
            return "factory.LazyFunction(uuid.uuid4)"
        else:
            # Fallback for other types - use sequence with string representation
            return "factory.Sequence(lambda n: f'unique{n}')"

    def _get_faker_for_field(self, field: FieldSpec) -> str:
        """Get Factory Boy faker for a field type."""
        # Handle unique fields with Sequence to avoid collisions
        if field.unique:
            return self._get_unique_faker(field)

        if field.type == FieldType.STRING:
            name_lower = field.name.lower()
            # Check for IP address fields
            if (
                ("ip" in name_lower and "address" in name_lower)
                or name_lower == "ip"
                or name_lower.endswith("_ip")
            ):
                return "factory.Faker('ipv4')"
            elif "email" in name_lower:
                return "factory.Faker('email')"
            elif "url" in name_lower:
                return "factory.Faker('url')"
            # For fields with patterns that suggest subdomain/domain names,
            # use a simple alphanumeric sequence instead of random strings
            elif field.pattern and "a-z" in field.pattern and "0-9" in field.pattern:
                # Pattern like ^[a-z0-9]... suggests alphanumeric only (no underscores)
                return "factory.Sequence(lambda n: f'item{n}')"
            elif "name" in name_lower:
                # For generic 'name' fields, check if there's a pattern constraint
                if field.pattern:
                    # If pattern restricts to alphanumeric, use sequence
                    return "factory.Sequence(lambda n: f'name{n}')"
                return "factory.Faker('name')"
            else:
                return f"factory.Faker('pystr', max_chars={field.max_length or 50})"

        elif field.type == FieldType.TEXT:
            return "factory.Faker('paragraph')"

        elif field.type == FieldType.INTEGER:
            min_val = int(field.min_value) if field.min_value is not None else 1
            max_val = int(field.max_value) if field.max_value is not None else 1000
            return f"factory.Faker('random_int', min={min_val}, max={max_val})"

        elif field.type in (FieldType.FLOAT, FieldType.DECIMAL):
            # Use constraint-aware generation if bounds are specified
            if field.min_value is not None or field.max_value is not None:
                min_float = float(field.min_value) if field.min_value is not None else 0.0
                max_float = float(field.max_value) if field.max_value is not None else 1000.0
                return f"factory.LazyFunction(lambda: round(random.uniform({min_float}, {max_float}), 2))"
            return "factory.Faker('pydecimal', left_digits=5, right_digits=2, positive=True)"

        elif field.type == FieldType.BOOLEAN:
            return "factory.Faker('boolean')"

        elif field.type == FieldType.DATETIME:
            return "factory.Faker('date_time')"

        elif field.type == FieldType.DATE:
            return "factory.Faker('date_object')"

        elif field.type == FieldType.TIME:
            return "factory.Faker('time_object')"

        elif field.type == FieldType.UUID:
            return "factory.Faker('uuid4')"

        elif field.type == FieldType.ENUM and field.enum_values:
            values = ", ".join(f"'{v}'" for v in field.enum_values)
            return f"factory.Faker('random_element', elements=[{values}])"

        elif field.type == FieldType.FOREIGN_KEY:
            return "factory.Faker('random_int', min=1, max=100)"

        elif field.type == FieldType.JSON:
            # If the field has json_item_type, it's a typed array
            if field.json_item_type:
                item_type = field.json_item_type.lower()
                if item_type in ("int", "integer"):
                    return (
                        "factory.LazyFunction(lambda: [random.randint(0, 255) for _ in range(3)])"
                    )
                elif item_type in ("float", "number"):
                    return "factory.LazyFunction(lambda: [round(random.random() * 100, 2) for _ in range(3)])"
                elif item_type in ("str", "string"):
                    return "factory.LazyFunction(lambda: ['item1', 'item2', 'item3'])"
                elif item_type in ("bool", "boolean"):
                    return "factory.LazyFunction(lambda: [True, False, True])"
                else:
                    return "factory.LazyFunction(lambda: ['item1', 'item2'])"
            return "factory.LazyFunction(lambda: {'key': 'value'})"

        return "factory.Faker('pystr')"

    def _get_test_imports(self, model: ModelSpec) -> str:
        """Get additional imports needed for test values based on field types."""
        imports = []

        has_datetime = any(f.type == FieldType.DATETIME for f in model.fields)
        has_date = any(f.type == FieldType.DATE for f in model.fields)
        has_time = any(f.type == FieldType.TIME for f in model.fields)
        has_uuid = any(f.type == FieldType.UUID for f in model.fields)

        datetime_types = []
        if has_datetime:
            datetime_types.append("datetime")
        if has_date:
            datetime_types.append("date")
        if has_time:
            datetime_types.append("time")

        if datetime_types:
            imports.append(f"from datetime import {', '.join(datetime_types)}")

        if has_uuid:
            imports.append("from uuid import uuid4")

        return "\n".join(imports)

    def _generate_service_tests(self, model: ModelSpec) -> GeneratedFile:
        """Generate unit tests for service class."""
        snake_name = to_snake_case(model.name)
        plural_name = pluralize(snake_name)
        project_name = self.get_package_name()

        # Get additional imports based on field types
        extra_imports = self._get_test_imports(model)
        if extra_imports:
            extra_imports = extra_imports + "\n"

        content = f'''"""Unit tests for {model.name}Service.

⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
"""

from __future__ import annotations

{extra_imports}import pytest
import pytest_asyncio

from {project_name}.services.{snake_name} import {model.name}Service
from {project_name}.schemas.{snake_name} import {model.name}Create, {model.name}Update
from tests.factories.{snake_name} import {model.name}Factory


class Test{model.name}Service:
    """Tests for {model.name}Service CRUD operations."""

    @pytest_asyncio.fixture
    async def service(self, db):
        """Create service instance."""
        {model.name}Factory._meta.sqlalchemy_session = db
        return {model.name}Service(db)

    async def test_create(self, service, db):
        """Test creating a {snake_name}."""
        data = {model.name}Create(
{self._build_create_test_data(model)}
        )

        result = await service.create(data=data)

        assert result is not None
        assert result.id is not None
{self._build_create_assertions(model)}

    async def test_get(self, service, db):
        """Test getting a {snake_name} by ID."""
        instance = {model.name}Factory.create()
        await db.commit()

        result = await service.get(instance.id)

        assert result is not None
        assert result.id == instance.id

    async def test_get_not_found(self, service):
        """Test getting a non-existent {snake_name}."""
        result = await service.get(99999)

        assert result is None

    async def test_list(self, service, db):
        """Test listing {plural_name}."""
        # Create test data
        for _ in range(5):
            {model.name}Factory.create()
        await db.commit()

        results = await service.list(skip=0, limit=10)

        assert len(results) >= 5

    async def test_update(self, service, db):
        """Test updating a {snake_name}."""
        instance = {model.name}Factory.create()
        await db.commit()

        data = {model.name}Update(
{self._build_update_test_data(model)}
        )

        result = await service.update(id=instance.id, data=data)

        assert result is not None

    async def test_update_not_found(self, service):
        """Test updating a non-existent {snake_name}."""
        data = {model.name}Update()

        result = await service.update(id=99999, data=data)

        assert result is None

    async def test_delete(self, service, db):
        """Test deleting a {snake_name}."""
        instance = {model.name}Factory.create()
        await db.commit()

        result = await service.delete(id=instance.id)

        assert result is True

    async def test_delete_not_found(self, service):
        """Test deleting a non-existent {snake_name}."""
        result = await service.delete(id=99999)

        assert result is False
'''

        return GeneratedFile(
            path=self.tests_path / "unit" / f"test_{snake_name}_service.py",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description=f"Service tests for {model.name}",
        )

    def _build_create_test_data(self, model: ModelSpec) -> str:
        """Build test data for create operation."""
        lines = []
        for field in model.fields:
            if field.required and not field.default:
                # Use unique suffix for fields that might have unique constraints
                suffix = "_create" if field.unique or "email" in field.name.lower() else ""
                value = self._get_test_value(field, suffix)
                lines.append(f"            {field.name}={value},")
        return "\n".join(lines)

    def _build_create_assertions(self, model: ModelSpec) -> str:
        """Build assertions for create test."""
        lines = []
        for field in model.fields:
            if field.required and not field.default:
                lines.append(f"        assert result.{field.name} is not None")
        return "\n".join(lines)

    def _build_update_test_data(self, model: ModelSpec) -> str:
        """Build test data for update operation."""
        # Just update the first non-unique field to avoid constraint issues
        for field in model.fields:
            # Skip unique fields for updates to avoid constraint violations
            if field.unique or "email" in field.name.lower():
                continue
            value = self._get_test_value(field, "_updated")
            return f"            {field.name}={value},"
        # If all fields are unique, update the first with a unique suffix
        for field in model.fields:
            value = self._get_test_value(field, "_updated")
            return f"            {field.name}={value},"
        return "            pass"

    def _get_test_value(self, field: FieldSpec, unique_suffix: str = "") -> str:
        """Get a test value for a field."""
        if field.type == FieldType.STRING:
            # Handle special string field patterns based on name or pattern
            name_lower = field.name.lower()

            # Check for IP address fields
            if (
                ("ip" in name_lower and "address" in name_lower)
                or name_lower == "ip"
                or name_lower.endswith("_ip")
            ):
                return '"192.168.1.1"'
            # Check for URL pattern or name
            elif (
                "url" in name_lower
                or "link" in name_lower
                or "website" in name_lower
                or (field.pattern and ("http" in field.pattern or "url" in field.pattern.lower()))
            ):
                return f'"https://example{unique_suffix}.com"'
            elif "email" in name_lower:
                return f'"test{unique_suffix}@example.com"'
            elif "phone" in name_lower:
                return '"+1234567890"'
            elif "ticker" in name_lower or "symbol" in name_lower:
                return f'"TEST{unique_suffix}"'
            elif "isin" in name_lower:
                # Use unique suffix for ISIN to avoid constraint violations
                # ISIN format: 12 characters (2 country + 9 NSIN + 1 check)
                suffix_char = unique_suffix.replace("_", "")[:1].upper() if unique_suffix else "0"
                return f'"US0000000{suffix_char}00"'
            elif "cusip" in name_lower:
                return '"000000000"'
            elif "currency" in name_lower:
                return '"USD"'
            elif "country" in name_lower:
                return '"US"'

            # For fields with patterns that restrict to alphanumeric, avoid underscores
            if field.pattern and "a-z" in field.pattern and "0-9" in field.pattern:
                suffix = unique_suffix.replace("_", "")
                base_value = f"testval{suffix}"
            else:
                base_value = f"test_value{unique_suffix}"
            # Respect min_length constraint
            if field.min_length and len(base_value) < field.min_length:
                pad = "a" if field.pattern and "_" not in field.pattern else "_"
                base_value = base_value + pad * (field.min_length - len(base_value))
            return f'"{base_value}"'
        elif field.type == FieldType.TEXT:
            return '"Test text content"'
        elif field.type == FieldType.INTEGER:
            # Respect min/max value constraints
            if field.min_value is not None or field.max_value is not None:
                min_val = int(field.min_value) if field.min_value is not None else 0
                max_val = int(field.max_value) if field.max_value is not None else 1000
                # Pick a value in the middle of the range
                test_val = (min_val + max_val) // 2
                return str(test_val)
            return "42"
        elif field.type in (FieldType.FLOAT, FieldType.DECIMAL):
            return "3.14"
        elif field.type == FieldType.BOOLEAN:
            return "True"
        elif field.type == FieldType.DATETIME:
            return "datetime(2024, 1, 15, 12, 0, 0)"
        elif field.type == FieldType.DATE:
            return "date(2024, 1, 15)"
        elif field.type == FieldType.TIME:
            return "time(12, 0, 0)"
        elif field.type == FieldType.UUID:
            return "uuid4()"
        elif field.type == FieldType.ENUM and field.enum_values:
            return f'"{field.enum_values[0]}"'
        elif field.type == FieldType.FOREIGN_KEY:
            return "1"
        elif field.type == FieldType.JSON:
            # If the field has json_item_type, it's a typed array
            if field.json_item_type:
                item_type = field.json_item_type.lower()
                if item_type in ("int", "integer"):
                    return "[34, 139, 34]"
                elif item_type in ("float", "number"):
                    return "[1.5, 2.7, 3.14]"
                elif item_type in ("str", "string"):
                    return '["item1", "item2", "item3"]'
                elif item_type in ("bool", "boolean"):
                    return "[True, False, True]"
                else:
                    return '["item1", "item2"]'
            return '{"key": "value"}'
        return '"test"'

    def _generate_api_tests(self, model: ModelSpec) -> GeneratedFile:
        """Generate API integration tests."""
        snake_name = to_snake_case(model.name)
        kebab_name = pluralize(snake_name).replace("_", "-")
        ops = model.rest.operations

        content = f'''"""API integration tests for {model.name}.

⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
"""

from __future__ import annotations

import pytest
import pytest_asyncio

from tests.factories.{snake_name} import {model.name}Factory


class Test{model.name}API:
    """API integration tests for {model.name} endpoints."""
'''

        # List test
        if ops.list:
            content += f'''
    @pytest.mark.asyncio
    async def test_list_{snake_name}s(self, client, db):
        """Test GET /{kebab_name}"""
        # Create test data
        {model.name}Factory._meta.sqlalchemy_session = db
        for _ in range(3):
            {model.name}Factory.create()
        await db.commit()

        response = await client.get("/api/{kebab_name}")

        assert response.status_code == 200
        data = response.json()
        assert "items" in data
        assert len(data["items"]) >= 3
'''

        # Get test
        if ops.read:
            content += f'''
    @pytest.mark.asyncio
    async def test_get_{snake_name}(self, client, db):
        """Test GET /{kebab_name}/{{id}}"""
        {model.name}Factory._meta.sqlalchemy_session = db
        instance = {model.name}Factory.create()
        await db.commit()

        response = await client.get(f"/api/{kebab_name}/{{instance.id}}")

        assert response.status_code == 200
        data = response.json()
        assert data["id"] == instance.id

    @pytest.mark.asyncio
    async def test_get_{snake_name}_not_found(self, client):
        """Test GET /{kebab_name}/{{id}} with invalid ID"""
        response = await client.get("/api/{kebab_name}/99999")

        assert response.status_code == 404
'''

        # Create test
        if ops.create:
            content += f'''
    @pytest.mark.asyncio
    async def test_create_{snake_name}(self, client):
        """Test POST /{kebab_name}"""
        payload = {{
{self._build_api_test_payload(model)}
        }}

        response = await client.post("/api/{kebab_name}", json=payload)

        assert response.status_code == 201
        data = response.json()
        assert "id" in data
'''

        # Update test
        if ops.update:
            content += f'''
    @pytest.mark.asyncio
    async def test_update_{snake_name}(self, client, db):
        """Test PATCH /{kebab_name}/{{id}}"""
        {model.name}Factory._meta.sqlalchemy_session = db
        instance = {model.name}Factory.create()
        await db.commit()

        payload = {{
{self._build_update_api_payload(model)}
        }}

        response = await client.patch(f"/api/{kebab_name}/{{instance.id}}", json=payload)

        assert response.status_code == 200
'''

        # Delete test - only if delete operation is enabled
        if ops.delete:
            content += f'''
    @pytest.mark.asyncio
    async def test_delete_{snake_name}(self, client, db):
        """Test DELETE /{kebab_name}/{{id}}"""
        {model.name}Factory._meta.sqlalchemy_session = db
        instance = {model.name}Factory.create()
        await db.commit()

        response = await client.delete(f"/api/{kebab_name}/{{instance.id}}")

        assert response.status_code == 204
'''

        return GeneratedFile(
            path=self.tests_path / "integration" / f"test_{snake_name}_api.py",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description=f"API tests for {model.name}",
        )

    def _build_api_test_payload(self, model: ModelSpec) -> str:
        """Build JSON payload for API test."""
        lines = []
        for field in model.fields:
            if field.required and not field.default:
                # Use unique suffix for fields that might have unique constraints
                suffix = "_api" if field.unique or "email" in field.name.lower() else ""
                value = self._get_json_test_value(field, suffix)
                lines.append(f'            "{field.name}": {value},')
        return "\n".join(lines)

    def _build_update_api_payload(self, model: ModelSpec) -> str:
        """Build update payload for API test."""
        # Skip unique fields to avoid constraint violations
        for field in model.fields:
            if field.unique or "email" in field.name.lower():
                continue
            value = self._get_json_test_value(field, "_updated")
            return f'            "{field.name}": {value},'
        # If all fields are unique, update first with unique suffix
        for field in model.fields:
            value = self._get_json_test_value(field, "_api_updated")
            return f'            "{field.name}": {value},'
        return '            "id": 1,'

    def _get_json_test_value(self, field: FieldSpec, unique_suffix: str = "") -> str:
        """Get a JSON-compatible test value for a field (for API payloads)."""
        if field.type == FieldType.STRING:
            # Handle special string field patterns based on name or pattern
            name_lower = field.name.lower()

            # Check for IP address fields
            if (
                ("ip" in name_lower and "address" in name_lower)
                or name_lower == "ip"
                or name_lower.endswith("_ip")
            ):
                return '"192.168.1.1"'
            # Check for URL pattern or name
            elif (
                "url" in name_lower
                or "link" in name_lower
                or "website" in name_lower
                or (field.pattern and ("http" in field.pattern or "url" in field.pattern.lower()))
            ):
                return f'"https://example{unique_suffix}.com"'
            elif "email" in name_lower:
                return f'"test{unique_suffix}@example.com"'
            elif "phone" in name_lower:
                return '"+1234567890"'
            elif "ticker" in name_lower or "symbol" in name_lower:
                return f'"TEST{unique_suffix}"'
            elif "isin" in name_lower:
                # Use unique suffix for ISIN to avoid constraint violations
                # ISIN format: 12 characters (2 country + 9 NSIN + 1 check)
                suffix_char = unique_suffix.replace("_", "")[:1].upper() if unique_suffix else "0"
                return f'"US0000000{suffix_char}00"'
            elif "cusip" in name_lower:
                return '"000000000"'
            elif "currency" in name_lower:
                return '"USD"'
            elif "country" in name_lower:
                return '"US"'

            # For fields with patterns that restrict to alphanumeric, avoid underscores
            if field.pattern and "a-z" in field.pattern and "0-9" in field.pattern:
                suffix = unique_suffix.replace("_", "")
                base_value = f"testval{suffix}"
            else:
                base_value = f"test_value{unique_suffix}"
            # Respect min_length constraint
            if field.min_length and len(base_value) < field.min_length:
                pad = "a" if field.pattern and "_" not in field.pattern else "_"
                base_value = base_value + pad * (field.min_length - len(base_value))
            return f'"{base_value}"'
        elif field.type == FieldType.TEXT:
            return '"Test text content"'
        elif field.type == FieldType.INTEGER:
            # Respect min/max value constraints
            if field.min_value is not None or field.max_value is not None:
                min_val = int(field.min_value) if field.min_value is not None else 0
                max_val = int(field.max_value) if field.max_value is not None else 1000
                # Pick a value in the middle of the range
                test_val = (min_val + max_val) // 2
                return str(test_val)
            return "42"
        elif field.type in (FieldType.FLOAT, FieldType.DECIMAL):
            return "3.14"
        elif field.type == FieldType.BOOLEAN:
            return "True"
        elif field.type == FieldType.DATETIME:
            return '"2024-01-15T12:00:00"'
        elif field.type == FieldType.DATE:
            return '"2024-01-15"'
        elif field.type == FieldType.TIME:
            return '"12:00:00"'
        elif field.type == FieldType.UUID:
            return '"550e8400-e29b-41d4-a716-446655440000"'
        elif field.type == FieldType.ENUM and field.enum_values:
            return f'"{field.enum_values[0]}"'
        elif field.type == FieldType.FOREIGN_KEY:
            return "1"
        elif field.type == FieldType.JSON:
            # If the field has json_item_type, it's a typed array
            if field.json_item_type:
                item_type = field.json_item_type.lower()
                if item_type in ("int", "integer"):
                    return "[34, 139, 34]"
                elif item_type in ("float", "number"):
                    return "[1.5, 2.7, 3.14]"
                elif item_type in ("str", "string"):
                    return '["item1", "item2", "item3"]'
                elif item_type in ("bool", "boolean"):
                    return "[true, false, true]"
                else:
                    return '["item1", "item2"]'
            return '{"key": "value"}'
        return '"test"'

    def _generate_graphql_tests(self, model: ModelSpec) -> GeneratedFile:
        """Generate GraphQL query tests.

        Uses flat query structure where model queries are directly on root Query type.
        """
        snake_name = to_snake_case(model.name)
        plural_snake = pluralize(snake_name)
        # camelCase names for GraphQL - must match GraphQL generator exactly
        # GraphQL generator uses: camel_plural = to_camel_case(pluralize(snake_name))
        camel_name = to_camel_case(model.name)
        camel_plural = to_camel_case(plural_snake)

        content = f'''"""GraphQL tests for {model.name}.

⚠️ AUTO-GENERATED BY PRISM - DO NOT EDIT
"""

from __future__ import annotations

import pytest
import pytest_asyncio

from tests.factories.{snake_name} import {model.name}Factory


class Test{model.name}GraphQL:
    """GraphQL tests for {model.name} queries and mutations."""

    @pytest.mark.asyncio
    async def test_query_{snake_name}(self, client, db):
        """Test {camel_name} query."""
        {model.name}Factory._meta.sqlalchemy_session = db
        instance = {model.name}Factory.create()
        await db.commit()

        query = """
            query Get{model.name}($id: Int!) {{
                {camel_name}(id: $id) {{
                    id
                }}
            }}
        """

        response = await client.post(
            "/graphql",
            json={{"query": query, "variables": {{"id": instance.id}}}},
        )

        assert response.status_code == 200
        data = response.json()
        assert "data" in data
        assert data["data"]["{camel_name}"] is not None, f"Query returned None. Errors: {{data.get('errors')}}"
        assert data["data"]["{camel_name}"]["id"] == instance.id

    @pytest.mark.asyncio
    async def test_query_{snake_name}s_list(self, client, db):
        """Test {camel_plural} list query."""
        {model.name}Factory._meta.sqlalchemy_session = db
        for _ in range(3):
            {model.name}Factory.create()
        await db.commit()

        query = """
            query List{model.name}s {{
                {camel_plural} {{
                    edges {{
                        node {{
                            id
                        }}
                    }}
                    totalCount
                }}
            }}
        """

        response = await client.post("/graphql", json={{"query": query}})

        assert response.status_code == 200
        data = response.json()
        assert "data" in data
        assert data["data"]["{camel_plural}"]["totalCount"] >= 3
'''

        return GeneratedFile(
            path=self.tests_path / "graphql" / f"test_{snake_name}.py",
            content=content,
            strategy=FileStrategy.ALWAYS_OVERWRITE,
            description=f"GraphQL tests for {model.name}",
        )


__all__ = ["BackendTestGenerator"]
