"""
Pytest fixtures for MealPlanner backend.

DB strategy
-----------
- If env ``TEST_DATABASE_URL`` is set, use it (must be a Postgres URL — the
  current Alembic migrations use ``postgresql.UUID``/``JSONB``/``ARRAY`` types
  which are not portable to SQLite).
- Else fall back to ``DATABASE_URL`` if it points at Postgres.
- Else skip Postgres-only tests (marker: ``requires_postgres``).

Each test using the ``db`` fixture runs inside a SAVEPOINT-style nested
transaction that rolls back on teardown so tests do not leak state.
"""

from __future__ import annotations

import os
import sys
import pathlib
import subprocess
from typing import Iterator

import pytest

# Ensure DATABASE_URL is set BEFORE importing app.config (Settings requires it).
# We default to the TEST_DATABASE_URL or a sentinel that lets imports succeed;
# tests that actually need the DB rely on the marker / fixture skip path below.
_DEFAULT_DSN = "postgresql://mealplanner:password@localhost:5432/mealplanner_test"
os.environ.setdefault(
    "DATABASE_URL",
    os.environ.get("TEST_DATABASE_URL", _DEFAULT_DSN),
)

# Make backend/ importable when pytest is invoked from repo root.
BACKEND_ROOT = pathlib.Path(__file__).resolve().parent.parent
if str(BACKEND_ROOT) not in sys.path:
    sys.path.insert(0, str(BACKEND_ROOT))

from sqlalchemy import create_engine, text  # noqa: E402
from sqlalchemy.orm import sessionmaker  # noqa: E402
from sqlalchemy.exc import OperationalError  # noqa: E402


def _resolve_test_dsn() -> str | None:
    dsn = os.environ.get("TEST_DATABASE_URL") or os.environ.get("DATABASE_URL")
    if not dsn:
        return None
    if not dsn.startswith(("postgresql://", "postgresql+psycopg2://", "postgresql+pg8000://")):
        return None
    return dsn


def _postgres_reachable(dsn: str) -> bool:
    try:
        driver = "postgresql+pg8000://" if "pg8000" in dsn else "postgresql+psycopg2://"
        eng = create_engine(
            dsn.replace("postgresql://", driver).replace("postgresql+psycopg2://", driver),
            pool_pre_ping=True,
        )
        with eng.connect() as conn:
            conn.execute(text("SELECT 1"))
        eng.dispose()
        return True
    except Exception:
        return False


_DSN = _resolve_test_dsn()
_PG_AVAILABLE = bool(_DSN) and _postgres_reachable(_DSN)


def pytest_collection_modifyitems(config, items):
    """Skip postgres-only tests when no live Postgres is available."""
    if _PG_AVAILABLE:
        return
    skip_pg = pytest.mark.skip(
        reason="Postgres not reachable; set TEST_DATABASE_URL to enable."
    )
    for item in items:
        if "requires_postgres" in item.keywords:
            item.add_marker(skip_pg)


def pytest_configure(config):
    config.addinivalue_line(
        "markers",
        "requires_postgres: test needs a live Postgres reachable via TEST_DATABASE_URL",
    )
    config.addinivalue_line(
        "markers",
        "scraper_offline: parser-only test; uses saved HTML fixture; no "
        "network or Playwright/Chromium required (CI-safe).",
    )
    config.addinivalue_line(
        "markers",
        "scraper_live: hits the live grocery website; requires Playwright "
        "and Chromium; skipped by default in CI.",
    )


# ---------------------------------------------------------------------------
# Schema bootstrap (session-scoped): run alembic upgrade head once per session.
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def _schema() -> Iterator[None]:
    if not _PG_AVAILABLE:
        yield
        return
    env = os.environ.copy()
    env["DATABASE_URL"] = _DSN  # Alembic env.py reads from settings.DATABASE_URL
    # alembic.ini lives in backend/, run from there.
    subprocess.run(
        ["alembic", "upgrade", "head"],
        cwd=str(BACKEND_ROOT),
        env=env,
        check=True,
    )
    yield
    # Best-effort cleanup so re-running the suite locally is idempotent.
    subprocess.run(
        ["alembic", "downgrade", "base"],
        cwd=str(BACKEND_ROOT),
        env=env,
        check=False,
    )


@pytest.fixture(scope="session")
def _engine(_schema):
    if not _PG_AVAILABLE:
        yield None
        return
    driver = "postgresql+pg8000://" if "pg8000" in _DSN else "postgresql+psycopg2://"
    eng = create_engine(
        _DSN.replace("postgresql://", driver).replace("postgresql+psycopg2://", driver),
        pool_pre_ping=True,
    )
    yield eng
    eng.dispose()


@pytest.fixture()
def db(_engine):
    """Per-test transactional session that rolls back at teardown."""
    if _engine is None:
        pytest.skip("Postgres not reachable")
    connection = _engine.connect()
    trans = connection.begin()
    Session = sessionmaker(bind=connection, autocommit=False, autoflush=False)
    session = Session()
    try:
        yield session
    finally:
        session.close()
        trans.rollback()
        connection.close()


@pytest.fixture()
def db_session():
    """Plain SessionLocal for tests that need to seed data outside the
    transactional ``db`` fixture (e.g. setting up rows the API will read)."""
    from app.database import SessionLocal

    s = SessionLocal()
    try:
        yield s
    finally:
        s.close()


@pytest.fixture()
def client(db):
    """TestClient with get_db overridden to yield the test session."""
    from fastapi.testclient import TestClient
    from app.main import app
    from app.database import get_db

    def _override():
        try:
            yield db
        finally:
            pass

    app.dependency_overrides[get_db] = _override
    try:
        with TestClient(app) as c:
            yield c
    finally:
        app.dependency_overrides.pop(get_db, None)


@pytest.fixture()
def client_no_db():
    """TestClient that does NOT require a live DB — for pure import/wiring smoke."""
    from fastapi.testclient import TestClient
    from app.main import app
    from app.database import get_db

    class _StubSession:
        def execute(self, *a, **kw):
            from sqlalchemy.engine import Result  # noqa: F401
            raise RuntimeError("DB not available in this fixture")

        def query(self, *a, **kw):
            raise RuntimeError("DB not available in this fixture")

        def close(self):
            pass

    def _override():
        yield _StubSession()

    app.dependency_overrides[get_db] = _override
    try:
        with TestClient(app) as c:
            yield c
    finally:
        app.dependency_overrides.pop(get_db, None)
