[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = ["app*"]
exclude = ["tests*", "uploads*", "docs*"]

[project]
name = "fast-next-backend"
version = "0.1.0"
description = "FastAPI backend for Fast-Next template"
readme = "README.md"
requires-python = ">=3.12"

# Core dependencies
dependencies = [
    # Core FastAPI framework and dependencies
    "fastapi>=0.115.8",
    "uvicorn>=0.34.0",
    "pydantic>=2.10.6",
    "pydantic-settings>=2.2.1",
    "python-multipart>=0.0.19",
    "fastapi-utils==0.8.0",
    # Database
    "sqlalchemy>=2.0.29",
    "alembic>=1.14.1",
    "psycopg2-binary>=2.9.9",
    "asyncpg>=0.29.0",
    "aiosqlite==0.21.0",
    # Environment configuration
    "python-dotenv>=1.0.1",
    # API utilities
    "email-validator>=2.1.0.post1",
    "ujson>=5.9.0",
    # CORS and security
    "starlette>=0.40.0",
    "starlette-csrf>=1.4.5",
    "slowapi>=0.1.9",
    # Utilities
    "httpx>=0.27.0",
    "tenacity>=8.2.3",
    "pytz>=2024.1",
    "pillow>=10.3.0",
    "apscheduler==3.11.0",
    # Security and authentication (pinned for reproducibility)
    "python-jose==3.4.0",
    "passlib==1.7.4",
    "bcrypt==4.2.1",
    "cryptography==44.0.1",
    # OAuth authentication
    "authlib>=1.3.0",
    # Celery for background task processing (Syndarix agent jobs)
    "celery[redis]>=5.4.0",
    "sse-starlette>=3.1.1",
    # MCP (Model Context Protocol) for AI agent tool integration
    "mcp>=1.0.0",
    # Circuit breaker pattern for resilient connections
    "pybreaker>=1.0.0",
    # YAML configuration support
    "pyyaml>=6.0.0",
]

# Development dependencies
[project.optional-dependencies]
dev = [
    # Testing
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.5",
    "pytest-cov>=4.1.0",
    "pytest-xdist>=3.8.0",
    "requests>=2.32.0",
    "freezegun~=1.5.1",

    # Development tools
    "ruff>=0.8.0",  # All-in-one: linting, formatting, import sorting
    "mypy>=1.8.0",  # Type checking
]

# E2E testing with real PostgreSQL (requires Docker)
e2e = [
    "testcontainers[postgres]>=4.0.0",
    "schemathesis>=3.30.0",
]

# ============================================================================
# Ruff Configuration - All-in-one linting, formatting, and import sorting
# ============================================================================
[tool.ruff]
target-version = "py312"
line-length = 88  # Black-compatible
indent-width = 4

# Exclude directories
exclude = [
    ".git",
    ".venv",
    "__pycache__",
    "*.egg-info",
    ".pytest_cache",
    ".mypy_cache",
    ".ruff_cache",
    "alembic/versions",  # Generated migration files
]

# ============================================================================
# Ruff Linting Rules
# ============================================================================
[tool.ruff.lint]
# Enable these rule sets:
# E/W   - pycodestyle errors and warnings
# F     - pyflakes (unused imports, variables, etc.)
# I     - isort (import sorting)
# N     - pep8-naming (naming conventions)
# UP    - pyupgrade (modern Python syntax)
# B     - flake8-bugbear (common bugs)
# C4    - flake8-comprehensions (list/dict comprehensions)
# PIE   - flake8-pie (misc lints)
# RUF   - Ruff-specific rules
# ASYNC - flake8-async (async best practices)
# S     - flake8-bandit (security)
# T20   - flake8-print (no print statements)
select = [
    "E",      # pycodestyle errors
    "W",      # pycodestyle warnings
    "F",      # pyflakes
    "I",      # isort
    "N",      # pep8-naming
    "UP",     # pyupgrade
    "B",      # flake8-bugbear
    "C4",     # flake8-comprehensions
    "PIE",    # flake8-pie
    "RUF",    # Ruff-specific
    "ASYNC",  # flake8-async
    "S",      # flake8-bandit (security)
]

# Ignore specific rules
ignore = [
    "E501",   # Line too long (handled by formatter)
    "S101",   # Use of assert (pytest uses asserts)
    "S104",   # Possible binding to all interfaces (FastAPI needs 0.0.0.0)
    "S105",   # Possible hardcoded password (false positives in field names)
    "S106",   # Possible hardcoded password (false positives in field names)
    "S603",   # subprocess without shell=True (safe usage)
    "S607",   # Starting a process with a partial path (safe usage)
    "B008",   # FastAPI Depends() in function defaults (required by framework)
    "B904",   # Exception chaining (overly strict for FastAPI error handlers)
]

# Allow autofix for all enabled rules
fixable = ["ALL"]
unfixable = []

# Per-file ignores for special cases
[tool.ruff.lint.per-file-ignores]
"app/alembic/env.py" = ["E402", "F403", "F405"]  # Alembic requires specific import order
"app/alembic/versions/*.py" = ["E402"]  # Migration files have specific structure
"tests/**/*.py" = ["S101", "N806", "B017", "N817", "S110", "ASYNC251", "RUF043"]  # pytest: asserts, CamelCase fixtures, blind exceptions, try-pass patterns, and async test helpers are intentional
"app/services/mcp/*.py" = ["ASYNC109", "S311", "RUF022"]  # timeout is config param not asyncio.timeout; random is ok for jitter; __all__ order is intentional for readability
"app/models/__init__.py" = ["F401"]  # __init__ files re-export modules
"app/models/base.py" = ["F401"]  # Re-exports Base for use by other models
"app/utils/test_utils.py" = ["N806"]  # SQLAlchemy session factories use CamelCase convention
"app/main.py" = ["N806"]  # Constants use UPPER_CASE convention

# ============================================================================
# Ruff Import Sorting (isort replacement)
# ============================================================================
[tool.ruff.lint.isort]
known-first-party = ["app", "tests"]
section-order = [
    "future",
    "standard-library",
    "third-party",
    "first-party",
    "local-folder",
]
combine-as-imports = true
force-wrap-aliases = true
split-on-trailing-comma = true

# ============================================================================
# Ruff Formatting (Black replacement)
# ============================================================================
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "lf"

# ============================================================================
# mypy Configuration - Type Checking
# ============================================================================
[tool.mypy]
python_version = "3.12"
warn_return_any = false  # SQLAlchemy queries return Any - overly strict
warn_unused_configs = true
disallow_untyped_defs = false  # Gradual typing - enable later
disallow_incomplete_defs = false
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
strict_equality = true
ignore_missing_imports = false
explicit_package_bases = true
namespace_packages = true

# Pydantic plugin for better validation
plugins = ["pydantic.mypy"]

# Per-module options
[[tool.mypy.overrides]]
module = "alembic.*"
ignore_errors = true

[[tool.mypy.overrides]]
module = "app.alembic.*"
ignore_errors = true

[[tool.mypy.overrides]]
module = "sqlalchemy.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "fastapi_utils.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "slowapi.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "jose.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "passlib.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "pydantic_settings.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "fastapi.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "apscheduler.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "starlette.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "authlib.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "celery.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "redis.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "sse_starlette.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "httpx.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "pybreaker.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "yaml.*"
ignore_missing_imports = true

# SQLAlchemy ORM models - Column descriptors cause type confusion
[[tool.mypy.overrides]]
module = "app.models.*"
disable_error_code = ["assignment", "arg-type", "return-value"]

# CRUD operations - Generic ModelType and SQLAlchemy Result issues
[[tool.mypy.overrides]]
module = "app.crud.*"
disable_error_code = ["attr-defined", "assignment", "arg-type", "return-value"]

# API routes - SQLAlchemy Column to Pydantic schema conversions
[[tool.mypy.overrides]]
module = "app.api.routes.*"
disable_error_code = ["arg-type", "call-arg", "call-overload", "assignment"]

# API dependencies - Similar SQLAlchemy Column issues
[[tool.mypy.overrides]]
module = "app.api.dependencies.*"
disable_error_code = ["arg-type"]

# FastAPI exception handlers have correct signatures despite mypy warnings
[[tool.mypy.overrides]]
module = "app.main"
disable_error_code = ["arg-type"]

# Auth service - SQLAlchemy Column issues
[[tool.mypy.overrides]]
module = "app.services.auth_service"
disable_error_code = ["assignment", "arg-type"]

# OAuth services - SQLAlchemy Column issues and unused type:ignore from library evolution
[[tool.mypy.overrides]]
module = "app.services.oauth_provider_service"
disable_error_code = ["assignment", "arg-type", "attr-defined", "unused-ignore"]

[[tool.mypy.overrides]]
module = "app.services.oauth_service"
disable_error_code = ["assignment", "arg-type", "attr-defined"]

# MCP services - circuit breaker and httpx client handling
[[tool.mypy.overrides]]
module = "app.services.mcp.*"
disable_error_code = ["attr-defined", "arg-type"]

# Test utils - Testing patterns
[[tool.mypy.overrides]]
module = "app.utils.auth_test_utils"
disable_error_code = ["assignment", "arg-type"]

# Test dependencies - ignore missing stubs
[[tool.mypy.overrides]]
module = "pytest_asyncio.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "schemathesis.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "testcontainers.*"
ignore_missing_imports = true

# Tests directory - relax type checking for test code
[[tool.mypy.overrides]]
module = "tests.*"
disable_error_code = ["arg-type", "union-attr", "return-value", "call-arg", "unused-ignore", "assignment", "var-annotated", "operator"]

# ============================================================================
# Pydantic mypy plugin configuration
# ============================================================================
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true

# ============================================================================
# Pytest Configuration
# ============================================================================
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "--disable-warnings",
    "-n", "auto",  # parallel execution
    "--strict-markers",
    "--tb=short",
    "--cov=app",
    "--cov-report=term-missing",
    "--cov-report=html",
]
markers = [
    "sqlite: marks tests that should run on SQLite (mocked).",
    "postgres: marks tests that require a real PostgreSQL database.",
    "e2e: marks end-to-end tests requiring Docker containers.",
    "schemathesis: marks Schemathesis-generated API tests.",
]
asyncio_default_fixture_loop_scope = "function"

# ============================================================================
# Coverage Configuration
# ============================================================================
[tool.coverage.run]
source = ["app"]
omit = [
    "*/tests/*",
    "*/__pycache__/*",
    "*/alembic/versions/*",
    "*/.venv/*",
    "app/init_db.py",  # CLI script for database initialization
]
branch = true

[tool.coverage.report]
precision = 2
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
    "@abstractmethod",
]
