#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
TESTPROJECT="$REPO_ROOT/testproject"

# Use uv if available, fall back to venv python, then system python
if command -v uv &>/dev/null; then
    # --frozen for the same reason as bin/run_tests: never let a routine
    # command rewrite uv.lock's self-version entry behind the caller's back.
    PYTHON="uv run --frozen python"
elif [ -f "$REPO_ROOT/.venv/bin/python" ]; then
    PYTHON="$REPO_ROOT/.venv/bin/python"
else
    PYTHON="python3"
fi

# ---------- per-checkout isolation ----------
# Derived, never inherited. A worktree is a COPY, so any value hand-written
# into a tracked config file would be silently shared with its parent — two
# suites then truncate and flushdb() each other mid-run. testit/testenv.py
# allocates a database name, a Redis index and a port keyed on this checkout's
# absolute path, recorded outside the tree in ~/.mojo/testenv.json.
#
# Invoked by FILE PATH, not `-m testit.testenv`: importing the package runs
# testit/__init__.py -> mojo.helpers.logit -> paths, which needs a configured
# project — and at this point the project is what we are about to create.
eval "$($PYTHON "$REPO_ROOT/testit/testenv.py" allocate \
    --root "$REPO_ROOT" --base mojo_test --format sh)"

if [ -z "${TESTENV_DB_NAME:-}" ] || [ -z "${TESTENV_PORT:-}" ] || [ -z "${TESTENV_REDIS_INDEX:-}" ]; then
    # Fail closed. Falling back to a shared default here is exactly how one
    # checkout DROPs the database another checkout is mid-run against.
    echo "ERROR: could not allocate a test environment for $REPO_ROOT" >&2
    echo "       run: $PYTHON $REPO_ROOT/testit/testenv.py allocate --base mojo_test" >&2
    exit 1
fi

echo "==> Creating testproject in $TESTPROJECT"
echo "    db=$TESTENV_DB_NAME  redis=$TESTENV_REDIS_INDEX  port=$TESTENV_PORT"

# Wipe if exists
if [ -d "$TESTPROJECT" ]; then
    echo "    Removing existing testproject/"
    rm -rf "$TESTPROJECT"
fi

# Create directory structure
mkdir -p "$TESTPROJECT/bin"
mkdir -p "$TESTPROJECT/config/settings/local"
mkdir -p "$TESTPROJECT/apps/tests"
mkdir -p "$TESTPROJECT/var/logs"

# ---------- testproject/bin/paths.py ----------
cat > "$TESTPROJECT/bin/paths.py" << 'PYEOF'
import sys
import os
from pathlib import Path

FILE = Path(__file__).resolve()
TESTPROJECT_ROOT = FILE.parent.parent        # django-mojo/testproject/
REPO_ROOT = TESTPROJECT_ROOT.parent          # django-mojo/

# mojo package + testit package both live at repo root
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

# settings module lives at testproject/config/
CONFIG_ROOT = TESTPROJECT_ROOT / "config"
if str(CONFIG_ROOT) not in sys.path:
    sys.path.insert(0, str(CONFIG_ROOT))

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
PYEOF

# ---------- testproject/bin/_asgi.py ----------
cat > "$TESTPROJECT/bin/_asgi.py" << 'PYEOF'
import paths  # testproject/bin/paths.py — bootstraps sys.path

from mojo.apps.realtime.routing import create_application
application = create_application()

# --- testit readiness signal -------------------------------------------------
# uvicorn --reload replaces the worker process on every django.conf change, but
# nothing downstream can tell the new worker from the old one: the old process
# keeps answering requests until it exits, so "the server responds" is not the
# same as "the server responds with my config". testit used to paper over that
# with a fixed 4.5s sleep per settings override.
#
# Writing our pid and the fingerprint of the config we actually loaded lets
# testit wait for exactly the right worker instead of guessing. It is written
# here rather than served over HTTP on purpose: a test that overrides settings
# can break URL routing, and a probe that lives in the URL map would then fail
# precisely when it is needed most.
def _write_testit_ready():
    import json, os, tempfile
    from mojo.helpers import paths as mojo_paths
    from mojo.helpers.settings import parser as settings_parser

    target = os.path.join(str(mojo_paths.VAR_ROOT), "asgi_ready.json")
    payload = {"pid": os.getpid(), "conf": settings_parser.conf_fingerprint()}
    # Atomic: testit polls this file and must never read a half-written one.
    fd, tmp = tempfile.mkstemp(dir=os.path.dirname(target), suffix=".tmp")
    try:
        with os.fdopen(fd, "w") as fh:
            json.dump(payload, fh)
        os.replace(tmp, target)
    except Exception:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


try:
    _write_testit_ready()
except Exception:
    # Never block the server from starting over a test-only convenience.
    pass
PYEOF

# ---------- testproject/config/settings/__init__.py ----------
cat > "$TESTPROJECT/config/settings/__init__.py" << 'PYEOF'
from mojo.helpers import paths, modules, settings

paths.configure_paths(__file__, 1)
modules.load_module_to_globals(paths, globals())
settings.load_settings_profile(globals())   # reads var/profile -> imports settings.local
settings.load_settings_config(globals())    # reads var/django.conf -> overrides (SECRET_KEY)
PYEOF

# ---------- testproject/config/settings/defaults.py ----------
cat > "$TESTPROJECT/config/settings/defaults.py" << 'PYEOF'
# Empty defaults — all settings come from settings.local
PYEOF

# ---------- testproject/config/settings/local/__init__.py ----------
cat > "$TESTPROJECT/config/settings/local/__init__.py" << 'PYEOF'
from .db import *

DEBUG = True
SECRET_KEY = "set-in-var-django-conf"

# Enable per-request X-Mojo-Test-* header overrides (geofence engine, account
# extension hooks, bouncer decorator). Gated by mojo.helpers.test_mode which
# ALSO requires loopback REMOTE_ADDR + no proxy chain. MUST be False (or
# absent) in production settings files.
MOJO_TEST_MODE = True

# Test-environment static config — set once so tests don't need server reloads
# to flip these per-test. Tests that need different values mutate
# django.conf.settings in-process or use test-mode headers where supported.
ALLOWED_REDIRECT_URLS = ["https://example.com/"]
# AUTH_HANDOFF_ALLOWED_URLS is deliberately NOT set here. Setting it at all is
# the opt-in switch for handoff destination enforcement, so leaving it unset
# means this server runs in MONITOR mode — exactly what a stock deployment gets
# on upgrade, which is the behavior most worth having under test by default.
# The enforcement tests opt in per-test with th.server_settings(...).
# Content-Security-Policy on the hosted auth pages is OPT-IN and ships off.
# Turned on here so tests/test_auth/csp.py can assert the real header on a live
# response; tests covering the shipped default do so in-process.
AUTH_CSP_ENABLED = True
GITHUB_CLIENT_ID = "test-client-id-123"

# Deterministic embeddings for the docit knowledge base — no AWS calls in
# tests. Tests exercising the provider-unavailable path override this via
# th.server_settings(EMBEDDINGS_PROVIDER="bedrock") (no AWS creds here).
EMBEDDINGS_PROVIDER = "mock"

AUTH_USER_MODEL = "account.User"
ROOT_URLCONF = "settings.urls"
ALLOWED_HOSTS = ["*"]
APPEND_SLASH = False
SITE_ID = 1

USE_TZ = True
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

ALLOW_ADMIN_SITE = False

INSTALLED_APPS = [
    "mojo.apps.account",
    "mojo.apps.logit",
    "mojo.apps.metrics",
    "mojo.apps.fileman",
    "mojo.apps.incident",
    "mojo.apps.aws",
    "mojo.apps.dnsman",
    "mojo.apps.edge",
    "mojo.apps.jobs",
    "mojo.apps.docit",
    "mojo.apps.docit_kb",
    "mojo.apps.realtime",
    "mojo.apps.phonehub",
    "mojo.apps.filevault",
    "mojo.apps.shortlink",
    "mojo.apps.chat",
    "mojo.apps.assistant",
    "mojo.apps.github",
    "django.contrib.humanize",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

MIDDLEWARE = [
    "mojo.middleware.cors.CORSMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "mojo.middleware.mojo.MojoMiddleware",
    "mojo.middleware.auth.AuthenticationMiddleware",
    "mojo.middleware.logging.LoggerMiddleware",
]

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

STATIC_URL = "/static/"

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
    "authorization",
    "content-type",
    "accept",
    "origin",
    "x-requested-with",
    "x-mojo-bouncer-token",
    "x-mojo-bouncer-duid",
]

LOGIT_DB_ALL = True
LOGIT_FILE_ALL = False
LOGIT_RETURN_REAL_ERROR = True
LOGIT_ASYNC_LOGGING = False   # synchronous in tests — no background thread surprises

LOGGING = {"version": 1, "disable_existing_loggers": False}

INCIDENT_EVENT_METRICS = False  # no metrics processing needed in tests

# Global per-identity API throttle (DM-042) — enforcement off in the suite so
# high-volume test modules never 429. Tests opt in per-request with the
# X-Mojo-Test-Api-Throttle header (accounting still runs regardless).
API_THROTTLE_ENABLED = False

# WS per-IP connect-rate limit off in the suite — every test module connects
# from 127.0.0.1, so a shared-IP limit would flake the whole run. The check
# itself is unit-tested in-process (tests/test_realtime/connection_limits.py).
WS_CONNECT_RATE_LIMIT = 0

# Mirrors mojo.apps.jobs.DEFAULT_CHANNELS plus the suite's own "email" channel.
# publish() routes to the channel it is given (no reroute onto "default"), so
# the drain in th.run_jobs() must list every channel the suite publishes to.
JOBS_CHANNELS = ["default", "priority", "cleanup", "incident_handlers",
                 "renditions", "certs", "webhooks", "webhook_fanout", "email"]
# publish() refuses channels outside DEFAULT_CHANNELS ∪ JOBS_CHANNELS ∪ this
# list ∪ '*-engine'. Every extra channel the suite publishes to must be
# declared here — a missing entry fails loudly as ValueError at publish.
# testit_run_jobs_helper / t906_* / t936_allowed are deliberately NOT in
# JOBS_CHANNELS: they exercise the allowed-but-not-consumed (cross-box) path.
JOBS_ALLOWED_CHANNELS = ["sms", "exec_broadcast", "exec_retry",
                         "testit_run_jobs_helper", "t906_firewall", "t906_a",
                         "t906_b", "t906_sched", "t906_seed", "t906_orphan",
                         "t936_allowed", "testit_aws_version_drift",
                         "testit_edge_deploy", "t1771_broadcast"]
JOBS_DEFAULT_MAX_RETRIES = 0
JOBS_ENGINE_MAX_WORKERS = 2
JOBS_RUNNER_HEARTBEAT_SEC = 5

# Both maps REPLACE their defaults wholesale (mojo/middleware/auth.py reads
# them with settings.get_static and a default dict — there is no merge), so the
# NAME_MAP must always list every scheme. Declaring only "grouptoken" here would
# silently un-map bearer/apikey and degrade every request to anonymous.
AUTH_BEARER_HANDLERS = {
    "grouptoken": "mojo.apps.account.services.group_token.validate_token",
}
AUTH_BEARER_NAME_MAP = {
    "bearer": "user",
    "apikey": "user",
    "grouptoken": "user",
}

ALLOW_SELF_DEACTIVATION = True

REST_AUTO_PREFIX = True

# Fake Twilio number so phonehub.send_sms() reaches the +1555 test-number shortcut
# instead of failing with "No from_number configured".
TWILIO_NUMBER = "+15550000000"
PYEOF

# ---------- testproject/config/settings/local/db.py ----------
# Unquoted heredoc: the isolation values below are substituted. The role stays
# `mojo_test` — one role owning every per-checkout database is fine, since the
# database name is the isolation boundary.
cat > "$TESTPROJECT/config/settings/local/db.py" << PYEOF
# Generated by bin/create_testproject — per-checkout, do not hand-edit.
# Values come from testit/testenv.py keyed on this checkout's path, so a
# worktree gets its own database, Redis index and port automatically.
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "$TESTENV_DB_NAME",
        "USER": "mojo_test",
        "PASSWORD": "mojo_test",
        "HOST": "localhost",
        "PORT": "5432",
    }
}

REDIS_DB = {"host": "localhost", "port": "6379"}

# The mojo redis helper reads this (mojo/helpers/redis/client.py). It is the
# axis every project shared before isolation existed: bin/testit.py calls
# flushdb() at the start of every run, so two suites on one index wipe each
# other.
REDIS_DB_INDEX = $TESTENV_REDIS_INDEX

CACHES = {
    "default": {
        "BACKEND": "mojo.cache.MojoRedisCache",
        "TIMEOUT": 300,
        "KEY_PREFIX": "mojot_$TESTENV_SLUG",
        "LOCATION": "localhost:6379",
    }
}
PYEOF

# ---------- testproject/config/settings/urls.py ----------
cat > "$TESTPROJECT/config/settings/urls.py" << 'PYEOF'
from mojo.helpers.response import JsonResponse, HttpResponse
from django.urls import path, include

urlpatterns = [
    path("", lambda request: HttpResponse("<html><body><h1>django-mojo test server</h1></body></html>", content_type="text/html")),
    path("", include("mojo.urls")),
]

def handler404(request, exception):
    return JsonResponse({"error": "Not found", "code": 404, "status": False}, status=404)
PYEOF

# ---------- testproject/config/dev_server.conf ----------
cat > "$TESTPROJECT/config/dev_server.conf" << EOF
host=127.0.0.1
port=$TESTENV_PORT
EOF

# ---------- testproject/manage.py ----------
cat > "$TESTPROJECT/manage.py" << 'PYEOF'
#!/usr/bin/env python
import sys
import os
from pathlib import Path

TESTPROJECT = Path(__file__).resolve().parent
REPO_ROOT = TESTPROJECT.parent

sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(TESTPROJECT / "config"))

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")

from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
PYEOF
chmod +x "$TESTPROJECT/manage.py"

# ---------- var/profile ----------
echo -n "local" > "$TESTPROJECT/var/profile"

# ---------- var/django.conf (with generated SECRET_KEY) ----------
SECRET_KEY=$($PYTHON -c "import secrets, string; print(''.join(secrets.choice(string.ascii_letters + string.digits + '!@#\$%^&*') for _ in range(50)))")
echo "SECRET_KEY = '${SECRET_KEY}'" > "$TESTPROJECT/var/django.conf"

echo "==> Setting up PostgreSQL database"
# Find psql
PSQL=$(command -v psql 2>/dev/null)
if [ -z "$PSQL" ]; then
    for v in 17 16 15 14; do
        if [ -x "/opt/homebrew/opt/postgresql@${v}/bin/psql" ]; then
            PSQL="/opt/homebrew/opt/postgresql@${v}/bin/psql"
            break
        fi
    done
fi
if [ -z "$PSQL" ]; then
    echo "ERROR: psql not found. Install PostgreSQL: brew install postgresql@17"
    exit 1
fi

# Create role and database (idempotent — drops and recreates DB each time)
$PSQL -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='mojo_test'" | grep -q 1 \
    || $PSQL -d postgres -c "CREATE ROLE mojo_test WITH LOGIN PASSWORD 'mojo_test';"

# Refuse to drop a database something is connected to. The name is derived per
# checkout so this should never fire — but if derivation ever breaks, the
# failure mode is destroying another session's database mid-run, and that
# deserves a guard rather than trust.
ACTIVE=$($PSQL -d postgres -tAc \
    "SELECT count(*) FROM pg_stat_activity WHERE datname='$TESTENV_DB_NAME';")
if [ "${ACTIVE:-0}" -gt 0 ]; then
    echo "ERROR: $TESTENV_DB_NAME has $ACTIVE active connection(s)." >&2
    echo "       Another run is using it. Stop it, or check:" >&2
    echo "       $PYTHON $REPO_ROOT/testit/testenv.py list" >&2
    exit 1
fi

$PSQL -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$TESTENV_DB_NAME'" | grep -q 1 \
    && $PSQL -d postgres -c "DROP DATABASE \"$TESTENV_DB_NAME\";"
$PSQL -d postgres -c "CREATE DATABASE \"$TESTENV_DB_NAME\" OWNER mojo_test;"

# Docit KB (mojo.apps.docit_kb) needs the pgvector extension. Create it as
# the admin user — the mojo_test role cannot — and fail with a clear hint
# when the extension is not installed in this PostgreSQL.
if $PSQL -d postgres -tc "SELECT 1 FROM pg_available_extensions WHERE name='vector'" | grep -q 1; then
    $PSQL -d "$TESTENV_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS vector;"
else
    echo "ERROR: pgvector extension not available in this PostgreSQL."
    echo "       Install it (brew install pgvector) or remove mojo.apps.docit_kb from INSTALLED_APPS."
    exit 1
fi

echo "==> Generating and running migrations"
$PYTHON "$TESTPROJECT/manage.py" makemigrations 2>&1
$PYTHON "$TESTPROJECT/manage.py" migrate --run-syncdb 2>&1

echo ""
echo "==> testproject/ created successfully"
echo "    Run ./bin/asgi_local start   to start the dev server (daemon)"
echo "    Run ./bin/asgi_local stop    to stop the dev server"
echo "    Run ./bin/asgi_local         to run in foreground (Ctrl-C to stop)"
echo "    Run ./bin/testit.py          to run the test suite"
