"""Shared test fixtures.

Tests run against an isolated in-memory SQLite database (one per test) so they
need no external services. ``DATABASE_URL`` is forced to SQLite *before* the app
is imported so the module-level engine in ``legate.db.session`` is harmless.
"""

from __future__ import annotations

import base64
import os
import tempfile

os.environ.setdefault("DATABASE_URL", "sqlite+pysqlite:///:memory:")
os.environ.setdefault("LEGATE_SECRET_KEY", "test-secret-key-do-not-use-in-prod")
os.environ.setdefault("LEGATE_ENV", "test")
# Tests exercise connectors against mock transports on placeholder hosts, so the
# SSRF guard is disabled here; ssrf-specific tests pass an explicit settings.
os.environ.setdefault("ALLOW_PRIVATE_CONNECTOR_URLS", "true")
# Credential vault master key (base64 of 32 bytes).
os.environ.setdefault("LEGATE_MASTER_KEY", base64.b64encode(b"0" * 32).decode())
# Agent + workflow runs execute inline so they complete deterministically.
os.environ.setdefault("AGENT_RUN_INLINE", "true")
os.environ.setdefault("WORKFLOW_RUN_INLINE", "true")
# RAG: offline embedder + in-memory vector store + inline ingestion + temp storage.
os.environ.setdefault("EMBEDDING_PROVIDER", "hashing")
os.environ.setdefault("VECTOR_STORE_BACKEND", "memory")
os.environ.setdefault("KB_INGEST_INLINE", "true")
os.environ.setdefault(
    "STORAGE_LOCAL_DIR", os.path.join(tempfile.gettempdir(), "legate_test_storage")
)

import httpx  # noqa: E402
import pytest  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402
from legate.agent import service as agent_service  # noqa: E402
from legate.config import get_settings  # noqa: E402
from legate.connectors import runtime as connector_runtime  # noqa: E402
from legate.db import models  # noqa: E402,F401  (register models on Base.metadata)
from legate.db.base import Base  # noqa: E402
from legate.db.session import get_db, get_session_factory  # noqa: E402
from legate.llm.fake import FakeLLMClient  # noqa: E402
from legate.main import app  # noqa: E402
from legate.rag.vectorstore.factory import reset_memory_store  # noqa: E402
from legate.security.oidc import set_oidc_client_factory  # noqa: E402
from legate.security.ratelimit import reset_limiter  # noqa: E402
from sqlalchemy import create_engine  # noqa: E402
from sqlalchemy.orm import sessionmaker  # noqa: E402
from sqlalchemy.pool import StaticPool  # noqa: E402


@pytest.fixture(autouse=True)
def _reset_vector_store():
    """Keep the process-wide in-memory vector store isolated between tests."""
    reset_memory_store()
    yield
    reset_memory_store()


@pytest.fixture(autouse=True)
def _reset_feature_flags():
    """Feature flags default off; individual tests flip them and we reset after."""
    yield
    settings = get_settings()
    settings.billing_enabled = False
    settings.rate_limit_enabled = False
    settings.oidc_enabled = False
    reset_limiter()
    set_oidc_client_factory(None)


@pytest.fixture()
def session_factory():
    """A fresh in-memory database, shared across connections via StaticPool."""
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
        future=True,
    )
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
    try:
        yield factory
    finally:
        Base.metadata.drop_all(engine)
        engine.dispose()


@pytest.fixture()
def db(session_factory):
    session = session_factory()
    try:
        yield session
    finally:
        session.close()


@pytest.fixture()
def client(session_factory):
    def override_get_db():
        session = session_factory()
        try:
            yield session
        finally:
            session.close()

    app.dependency_overrides[get_db] = override_get_db
    # Background/inline agent runs open their own session on the same database.
    app.dependency_overrides[get_session_factory] = lambda: session_factory
    with TestClient(app) as test_client:
        yield test_client
    app.dependency_overrides.clear()


@pytest.fixture()
def set_agent_responses():
    """Install a scripted FakeLLMClient for agent runs; reset afterwards."""

    def _set(responses: list[str]) -> FakeLLMClient:
        fake = FakeLLMClient(responses)
        agent_service.set_llm_factory(lambda provider, model: fake)
        return fake

    yield _set
    agent_service.set_llm_factory(None)


@pytest.fixture()
def set_connector_http():
    """Route connector HTTP calls through a MockTransport handler; reset afterwards."""

    def _set(handler):
        connector_runtime.set_async_client_factory(
            lambda: httpx.AsyncClient(transport=httpx.MockTransport(handler))
        )

    yield _set
    connector_runtime.set_async_client_factory(None)


class Helpers:
    """Convenience flows used across the integration tests."""

    @staticmethod
    def register(client, email, password="password123", name="Test User"):
        resp = client.post(
            "/auth/register",
            json={"email": email, "password": password, "name": name},
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def login(client, email, password="password123"):
        resp = client.post("/auth/login", json={"email": email, "password": password})
        assert resp.status_code == 200, resp.text
        return resp.json()

    @staticmethod
    def headers(token):
        return {"Authorization": f"Bearer {token}"}

    @staticmethod
    def create_workspace(client, access_token, name="Acme"):
        resp = client.post(
            "/workspaces",
            json={"name": name},
            headers=Helpers.headers(access_token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def bootstrap_owner(client, email="owner@example.com", name="Acme"):
        """Register a user, create a workspace, return a workspace-scoped token."""
        Helpers.register(client, email)
        tokens = Helpers.login(client, email)
        created = Helpers.create_workspace(client, tokens["access_token"], name=name)
        return {
            "user_email": email,
            "refresh_token": tokens["refresh_token"],
            "workspace": created["workspace"],
            "access_token": created["access_token"],  # scoped to the new workspace
        }

    @staticmethod
    def add_member(client, owner_token, *, email, role, password="password123"):
        """Register a member, invite them with a role, and return their scoped token."""
        Helpers.register(client, email, password=password)
        invite = client.post(
            "/members/invite",
            json={"email": email, "role": role},
            headers=Helpers.headers(owner_token),
        )
        assert invite.status_code == 201, invite.text
        tokens = Helpers.login(client, email, password=password)
        return {"membership": invite.json(), "access_token": tokens["access_token"]}

    @staticmethod
    def create_agent(client, token, *, name="Helper", tools=None, **kwargs):
        body = {"name": name, "model": "fake-model", "tools": tools or [], **kwargs}
        resp = client.post("/agents", json=body, headers=Helpers.headers(token))
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def run_agent(client, token, agent_id, *, input=""):
        resp = client.post(
            f"/agents/{agent_id}/run",
            json={"input": input},
            headers=Helpers.headers(token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def create_kb(client, token, *, name="KB", **kwargs):
        resp = client.post(
            "/knowledge-bases",
            json={"name": name, **kwargs},
            headers=Helpers.headers(token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def upload_document(
        client, token, kb_id, *, filename="doc.txt", content=b"", content_type="text/plain"
    ):
        resp = client.post(
            f"/knowledge-bases/{kb_id}/documents",
            files={"file": (filename, content, content_type)},
            headers=Helpers.headers(token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def install_connector(client, token, *, type, name="Conn", config=None, secrets=None):
        resp = client.post(
            "/connectors",
            json={"type": type, "name": name, "config": config or {}, "secrets": secrets or {}},
            headers=Helpers.headers(token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def create_workflow(
        client, token, *, name="WF", definition=None, trigger_type="manual", trigger_config=None
    ):
        resp = client.post(
            "/workflows",
            json={
                "name": name,
                "definition": definition or {"nodes": [], "edges": []},
                "trigger_type": trigger_type,
                "trigger_config": trigger_config or {},
            },
            headers=Helpers.headers(token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()

    @staticmethod
    def run_workflow(client, token, workflow_id, *, input=None):
        resp = client.post(
            f"/workflows/{workflow_id}/run",
            json={"input": input or {}},
            headers=Helpers.headers(token),
        )
        assert resp.status_code == 201, resp.text
        return resp.json()


@pytest.fixture()
def helpers():
    return Helpers
