"""
Shared fixtures for the pytest suite. There is no isolated test database —
tests run against the same dev Postgres instance the app itself uses (see
the plan's Pillar 5 note on this tradeoff), so every fixture provisions
disposable, clearly-named rows and tears them down afterward rather than
relying on a schema that gets wiped between runs.

`test_tenant` mirrors scripts/provision_tenant.py: it opens its own session
directly against app.core.database.engine (the same connecting Postgres
role used everywhere else in this app, which is BYPASSRLS/superuser in dev
— see db/schema.sql's RLS section) rather than going through the API, since
there is no bootstrap route to create a tenant from nothing.
"""

import os
import shutil
import uuid
from datetime import datetime, timezone

# This run's background jobs go to their own queue, so a dev server sharing
# the database never picks them up (app/core/jobs.py).
os.environ["JOB_QUEUE"] = "pytest"
from collections.abc import AsyncGenerator

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import delete, text

from app.crud import field_indexes
from app.crud.entity_storage import drop_tenant_storage
from app.core.auth import create_access_token, hash_password
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

from app.core.config import settings
from app.main import app
from app.models.identity import Role, Tenant, User, UserRole
from app.routers.uploads import UPLOAD_ROOT

# Tests set up and inspect data directly, across tenants — so their own
# sessions connect as the database superuser, which row-level security
# doesn't bind. The API under test connects as its own ordinary role, which
# forced RLS does (db/schema.sql), so every API call here runs isolated.
_superuser_engine = create_async_engine(
    make_url(settings.database_url).set(username=settings.provisioning_superuser, password=settings.provisioning_superuser_password),
    pool_pre_ping=True,
)
AsyncSessionLocal = async_sessionmaker(_superuser_engine, expire_on_commit=False, autoflush=False)

TEST_ADMIN_PASSWORD = "pytest-password-123"

# Tenant-scoped tables that might accumulate rows during a test, in an order
# safe to delete in (children before the parents/tables they reference).
# Mirrors the ARRAY[...] lists in db/schema.sql's RLS/trigger setup.
_CHILD_TABLES_DELETE_ORDER = [
    "task_runs",
    "form_submission_reviews",
    "summary_cache",
    "workflow_delegations",
    "public_data_sources",
    "form_verifications",
    "saved_views",
    "user_sessions",
    "license_requests",
    "license_files",
    "jobs",
    "releases",
    "notifications",
    "step_history",
    "form_submissions",
    "workflow_instances",
    "widgets",
    "master_records",
    "audit_log_entries",
    "entity_migrations",
    "entity_tables",
    "ai_assistant_sessions",
    "datasets",
    "reports",
    "payment_transactions",
    "sequence_counters",
    "secrets",
    "record_access_policies",
    "page_versions",
    "app_import_links",
    "apps",
    "data_queries",
    "report_templates",
    "connectors",
    "components",
    "pages",
    "menus",
    "dashboards",
    # workflows first: deleting one cascades to its workflow_step_forms,
    # which reference forms.
    "workflows",
    "forms",
    "scheduled_jobs",
    "tasks",
    "masters",
    "uploaded_files",
    "workspaces",
]


@pytest_asyncio.fixture
async def client() -> AsyncGenerator[AsyncClient, None]:
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac


@pytest_asyncio.fixture
async def test_tenant() -> AsyncGenerator[dict, None]:
    """Provisions a throwaway tenant + a real (non-system) Administrator-like
    role + an admin user with a known password, then deletes everything it
    touched (including anything a test created under the tenant) afterward."""
    unique = uuid.uuid4().hex[:8]
    async with AsyncSessionLocal() as db:
        async with db.begin():
            tenant = Tenant(name=f"Pytest Tenant {unique}", status="active", license_set_at=datetime.now(timezone.utc))
            db.add(tenant)
            await db.flush()

            admin_role = Role(
                tenant_id=tenant.id,
                name="Pytest Administrator",
                is_system_role=True,
                action_matrix={},
            )
            db.add(admin_role)
            await db.flush()

            admin = User(
                tenant_id=tenant.id,
                name="Pytest Admin",
                email=f"pytest-admin-{unique}@example.com",
                password_hash=hash_password(TEST_ADMIN_PASSWORD),
                status="active",
            )
            db.add(admin)
            await db.flush()

            db.add(UserRole(user_id=admin.id, role_id=admin_role.id))
            await db.flush()

            tenant_id, admin_id, admin_role_id = tenant.id, admin.id, admin_role.id
            admin_email = admin.email

    yield {
        "tenant_id": tenant_id,
        "admin_id": admin_id,
        "admin_role_id": admin_role_id,
        "admin_email": admin_email,
        "admin_password": TEST_ADMIN_PASSWORD,
    }

    async with AsyncSessionLocal() as db:
        async with db.begin():
            # Datasources a test stored as tables: their tables and sync triggers.
            await drop_tenant_storage(db, tenant_id)
            for table in _CHILD_TABLES_DELETE_ORDER:
                await db.execute(text(f"DELETE FROM {table} WHERE tenant_id = :tid"), {"tid": str(tenant_id)})
            await db.execute(delete(UserRole).where(UserRole.user_id == admin_id))
            await db.execute(delete(User).where(User.tenant_id == tenant_id))
            await db.execute(text("DELETE FROM vendors WHERE tenant_id = :tid"), {"tid": str(tenant_id)})
            await db.execute(delete(Role).where(Role.tenant_id == tenant_id))
            await db.execute(delete(Tenant).where(Tenant.id == tenant_id))

    # This database is shared with the dev server, so nothing stays behind on its record tables.
    await field_indexes.wait_for_pending()
    shutil.rmtree(UPLOAD_ROOT / str(tenant_id), ignore_errors=True)


@pytest_asyncio.fixture
async def admin_token(test_tenant: dict) -> str:
    """A ready-to-use bearer token for the throwaway admin — minted directly
    rather than via POST /auth/login so tests that aren't specifically about
    login itself don't pay for an extra round trip."""
    return create_access_token(tenant_id=test_tenant["tenant_id"], user_id=test_tenant["admin_id"], vendor_id=None)


@pytest.fixture
def auth_headers(admin_token: str) -> dict[str, str]:
    return {"Authorization": f"Bearer {admin_token}"}
