# E2E Tests Justfile
# All E2E testing utilities: build, run, test, database dump/restore

set dotenv-load := true

# =============================================================================
# Configuration
# =============================================================================

# Docker settings
network := "baserow-e2e"
# Use same image names as `just build backend ci` and `just build web-frontend ci`
backend_image := "baserow/backend:ci"
frontend_image := "baserow/web-frontend:ci"
db_image := "pgvector/pgvector:pg${POSTGRES_IMAGE_VERSION:-14}"
redis_image := "redis:6"
# Answers the HTTP requests button field actions make, so nothing in the suite
# depends on the internet. It is httpbin, the same service the tests can be
# pointed at publicly when running against a dev stack.
# Pinned by digest: httpbin publishes no tag but `latest`.
http_stub_image := "kennethreitz/httpbin@sha256:599fe5e5073102dbb0ee3dbb65f049dab44fa9fc251f6835c9990f8fb196a72b"
# Catches the mail the email action sends, so a test can read it back.
mail_image := "mailhog/mailhog:v1.0.1"
# Answers the Slack API for the button field's Slack action, from e2e-tests/stubs/slack.
slack_stub_image := "wiremock/wiremock:3.13.1"
# Holds a button's request open until a test releases it, from e2e-tests/stubs/barrier.
barrier_stub_image := "node:24.22.1-alpine@sha256:6a1c190e17215d844926b0677f4bf87273ff7d5e54db06d3b5b5863ab0b09a3d"

# Host ports (override with env vars to avoid collisions with a running dev stack).
# Keep these well above the documented dev ranges so 8030-8060/3030-3060 stay
# free for additional dev instances:
# - default dev: 8000/3000
# - alternate dev A: 8010/3010
# - alternate dev B: 8020/3020
backend_port := env_var_or_default("E2E_BACKEND_PORT", "8070")
frontend_port := env_var_or_default("E2E_FRONTEND_PORT", "3070")
frontend_cookie_prefix := env_var_or_default("E2E_FRONTEND_COOKIE_PREFIX", "baserow_e2e_")
# MailHog's HTTP API, which the tests read sent mail from.
mail_api_port := env_var_or_default("E2E_MAIL_API_PORT", "8075")
# The barrier stub's control API, which the tests hold and release requests with.
barrier_control_port := env_var_or_default("E2E_BARRIER_CONTROL_PORT", "8076")
# How many external clicks one user gets per minute. Low so a test can reach it
# in a few clicks; clicks that stay inside Baserow are not counted at all.
button_rate_limit := env_var_or_default("E2E_BUTTON_RATE_LIMIT", "3")
public_backend_url := env_var_or_default("PUBLIC_BACKEND_URL", "http://localhost:" + backend_port)
public_frontend_url := env_var_or_default("PUBLIC_WEB_FRONTEND_URL", "http://localhost:" + frontend_port)
builder_preview_url := env_var_or_default("BASEROW_BUILDER_PREVIEW_URL", public_frontend_url)

# Database dump settings
dump_file := "fixtures/e2e-db.dump"
dump_network := "e2e-dump-net"
dump_db := "e2e-dump-db"

# Default: show help
default:
    @just --list

# =============================================================================
# Build
# =============================================================================

# Build CI images for E2E testing (delegates to root justfile)
[doc("Build backend and frontend CI images for E2E tests")]
build *ARGS:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "$(git rev-parse --show-toplevel)"

    echo "Building backend and frontend CI images..."
    echo ""

    # Use root justfile build commands
    just build backend ci {{ ARGS }} &
    BACKEND_PID=$!

    just build web-frontend ci {{ ARGS }} &
    FRONTEND_PID=$!

    # Wait for both to complete
    BACKEND_EXIT=0
    FRONTEND_EXIT=0
    wait $BACKEND_PID || BACKEND_EXIT=$?
    wait $FRONTEND_PID || FRONTEND_EXIT=$?

    echo ""
    if [ $BACKEND_EXIT -ne 0 ]; then
        echo "ERROR: Backend build failed (exit code: $BACKEND_EXIT)"
    fi
    if [ $FRONTEND_EXIT -ne 0 ]; then
        echo "ERROR: Frontend build failed (exit code: $FRONTEND_EXIT)"
    fi
    if [ $BACKEND_EXIT -ne 0 ] || [ $FRONTEND_EXIT -ne 0 ]; then
        exit 1
    fi

    echo "Images built:"
    echo "  {{ backend_image }}"
    echo "  {{ frontend_image }}"

# Check if CI images exist, build them if not
[private]
_ensure-images:
    #!/usr/bin/env bash
    set -euo pipefail

    NEED_BUILD=false

    if ! docker image inspect {{ backend_image }} > /dev/null 2>&1; then
        echo "Backend CI image not found: {{ backend_image }}"
        NEED_BUILD=true
    fi

    if ! docker image inspect {{ frontend_image }} > /dev/null 2>&1; then
        echo "Frontend CI image not found: {{ frontend_image }}"
        NEED_BUILD=true
    fi

    if [ "$NEED_BUILD" = true ]; then
        echo ""
        echo "Building missing CI images..."
        just build
    fi

# =============================================================================
# Run Environment
# =============================================================================

# Start E2E environment (DB, Redis, backend, frontend)
# Uses pre-generated database dump for fast startup
[doc("Start E2E environment containers")]
up: _ensure-images
    #!/usr/bin/env bash
    set -euo pipefail
    cd "$(git rev-parse --show-toplevel)"

    DUMP_FILE="e2e-tests/{{ dump_file }}"

    # Check if dump file exists
    if [ ! -f "$DUMP_FILE" ]; then
        echo "Error: Database dump not found at $DUMP_FILE"
        echo "Run 'just e2e db-dump' to generate it first."
        exit 1
    fi

    # Clean up any stopped containers from previous runs
    docker rm -f e2e-frontend e2e-backend e2e-celery e2e-redis e2e-db e2e-httpbin e2e-slack-stub e2e-barrier e2e-mail 2>/dev/null || true
    docker volume rm e2e-media 2>/dev/null || true

    # Create network if it doesn't exist
    docker network create {{ network }} 2>/dev/null || true

    # Start PostgreSQL
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-db$"; then
        echo "Starting PostgreSQL..."
        docker run -d --network={{ network }} --name e2e-db \
            -e POSTGRES_USER=baserow \
            -e POSTGRES_PASSWORD=baserow \
            -e POSTGRES_DB=baserow \
            --tmpfs /var/lib/postgresql/data:size=2G \
            {{ db_image }}
    fi

    # Start Redis
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-redis$"; then
        echo "Starting Redis..."
        docker run -d --network={{ network }} --name e2e-redis \
            --tmpfs /data:size=256m \
            {{ redis_image }}
    fi

    # Start the HTTP stub the button field's HTTP actions call
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-httpbin$"; then
        echo "Starting HTTP stub..."
        docker run -d --network={{ network }} --name e2e-httpbin \
            {{ http_stub_image }}
    fi

    # Start the Slack stub the button field's Slack action posts to
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-slack-stub$"; then
        echo "Starting Slack stub..."
        docker run -d --network={{ network }} --name e2e-slack-stub \
            -v "$(pwd)/e2e-tests/stubs/slack:/home/wiremock:ro" \
            {{ slack_stub_image }}
    fi

    # Start the barrier stub the button field's concurrency tests hold requests on
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-barrier$"; then
        echo "Starting barrier stub..."
        docker run -d --network={{ network }} --name e2e-barrier \
            -p {{ barrier_control_port }}:8080 \
            -v "$(pwd)/e2e-tests/stubs/barrier:/stub:ro" \
            {{ barrier_stub_image }} node /stub/server.mjs
    fi

    # Start MailHog, which catches what the email action sends
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-mail$"; then
        echo "Starting MailHog..."
        docker run -d --network={{ network }} --name e2e-mail \
            -p {{ mail_api_port }}:8025 \
            {{ mail_image }}
    fi

    # Create shared tmpfs volume for user files (backend + celery)
    # Use a named volume with tmpfs driver for speed and proper permissions
    docker volume rm e2e-media 2>/dev/null || true
    docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=512m,uid=9999,gid=9999 e2e-media

    # Wait for PostgreSQL to be ready
    echo "Waiting for PostgreSQL..."
    POSTGRES_READY=false
    for i in {1..30}; do
        if docker exec e2e-db pg_isready -U baserow > /dev/null 2>&1; then
            POSTGRES_READY=true
            break
        fi
        sleep 1
    done
    if [ "$POSTGRES_READY" != true ]; then
        echo "Timed out waiting for PostgreSQL to accept connections."
        exit 1
    fi

    BASEROW_DB_CREATED=false
    for i in {1..30}; do
        if docker exec e2e-db psql -U baserow -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='baserow'" | grep -q 1; then
            BASEROW_DB_CREATED=true
            break
        fi
        sleep 1
    done
    if [ "$BASEROW_DB_CREATED" != true ]; then
        echo "Timed out waiting for PostgreSQL to create the baserow database."
        exit 1
    fi

    # Restore database from dump (fast - migrations pre-applied)
    echo "Restoring database from dump..."
    docker exec e2e-db psql -U baserow -d postgres -c "DROP DATABASE IF EXISTS baserow;" > /dev/null
    docker exec e2e-db psql -U baserow -d postgres -c "CREATE DATABASE baserow;" > /dev/null
    docker exec -i e2e-db pg_restore -U baserow -d baserow --no-owner --no-acl < "$DUMP_FILE" 2>/dev/null || true
    echo "Database restored."

    # Start backend (migrations run on startup to apply any new ones + sync templates)
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-backend$"; then
        echo "Starting backend..."
        docker run -d --network={{ network }} --name e2e-backend \
            -v e2e-media:/baserow/media \
            -e DATABASE_HOST=e2e-db \
            -e DATABASE_NAME=baserow \
            -e DATABASE_USER=baserow \
            -e DATABASE_PASSWORD=baserow \
            -e REDIS_URL=redis://e2e-redis:6379 \
            -e SECRET_KEY=test \
            -e DJANGO_SETTINGS_MODULE=baserow.config.settings.e2e \
            -e MIGRATE_ON_STARTUP=true \
            -e FEATURE_FLAGS="*" \
            -e BASEROW_INTEGRATIONS_ALLOW_PRIVATE_ADDRESS=true \
            -e BASEROW_INTEGRATIONS_SLACK_API_URL=http://e2e-slack-stub:8080/api \
            -e EMAIL_SMTP=yes \
            -e EMAIL_SMTP_HOST=e2e-mail \
            -e EMAIL_SMTP_PORT=1025 \
            -e BASEROW_DATABASE_BUTTON_DISPATCH_USER_RATE_LIMITS={{ button_rate_limit }}/m \
            -e BASEROW_FRONTEND_COOKIE_PREFIX={{ frontend_cookie_prefix }} \
            -e BASEROW_MCP_OAUTH_ENABLED=true \
            -e PRIVATE_BACKEND_URL=http://e2e-backend:8000 \
            -e PUBLIC_BACKEND_URL="{{ public_backend_url }}" \
            -e PUBLIC_WEB_FRONTEND_URL="{{ public_frontend_url }}" \
            -e BASEROW_BUILDER_PREVIEW_URL="{{ builder_preview_url }}" \
            -e BASEROW_FRONTEND_COOKIE_PREFIX={{ frontend_cookie_prefix }} \
            -p {{ backend_port }}:8000 \
            {{ backend_image }} gunicorn
    fi

    # Start celery worker (shares media volume with backend)
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-celery$"; then
        echo "Starting celery worker..."
        docker run -d --network={{ network }} --name e2e-celery \
            -v e2e-media:/baserow/media \
            -e DATABASE_HOST=e2e-db \
            -e DATABASE_NAME=baserow \
            -e DATABASE_USER=baserow \
            -e DATABASE_PASSWORD=baserow \
            -e REDIS_URL=redis://e2e-redis:6379 \
            -e SECRET_KEY=test \
            -e DJANGO_SETTINGS_MODULE=baserow.config.settings.e2e \
            -e FEATURE_FLAGS="*" \
            -e BASEROW_INTEGRATIONS_ALLOW_PRIVATE_ADDRESS=true \
            -e BASEROW_INTEGRATIONS_SLACK_API_URL=http://e2e-slack-stub:8080/api \
            -e EMAIL_SMTP=yes \
            -e EMAIL_SMTP_HOST=e2e-mail \
            -e EMAIL_SMTP_PORT=1025 \
            -e BASEROW_DATABASE_BUTTON_DISPATCH_USER_RATE_LIMITS={{ button_rate_limit }}/m \
            -e BASEROW_RUN_MINIMAL=yes \
            -e BASEROW_AMOUNT_OF_WORKERS=1 \
            {{ backend_image }} celery-worker
    fi

    # Start frontend
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-frontend$"; then
        echo "Starting frontend..."
        docker run -d --network={{ network }} --name e2e-frontend \
            -e PUBLIC_BACKEND_URL="{{ public_backend_url }}" \
            -e PUBLIC_WEB_FRONTEND_URL="{{ public_frontend_url }}" \
            -e BASEROW_BUILDER_PREVIEW_URL="{{ builder_preview_url }}" \
            -e PRIVATE_BACKEND_URL=http://e2e-backend:8000 \
            -e BASEROW_FRONTEND_COOKIE_PREFIX={{ frontend_cookie_prefix }} \
            -e FEATURE_FLAGS="*" \
            -p {{ frontend_port }}:3000 \
            {{ frontend_image }} nuxt-prod
    fi

    echo ""
    echo "E2E environment started. Containers:"
    docker ps --filter "name=e2e-" --format "table {{ '{{.Names}}\t{{.Status}}\t{{.Ports}}' }}"
    echo ""
    echo "Frontend: {{ public_frontend_url }}"
    echo "Preview:  {{ builder_preview_url }}"
    echo "Backend:  {{ public_backend_url }}"
    echo ""
    echo "View logs:  just e2e logs"
    echo "Run tests:  just e2e test"
    echo "Stop:       just e2e down"

# Stop and remove E2E containers
[doc("Stop E2E environment")]
down:
    #!/usr/bin/env bash
    echo "Stopping E2E containers..."
    docker rm -f e2e-frontend e2e-backend e2e-celery e2e-redis e2e-db e2e-s3mock e2e-httpbin e2e-slack-stub e2e-barrier e2e-mail 2>/dev/null || true
    docker volume rm e2e-media 2>/dev/null || true
    docker network rm {{ network }} 2>/dev/null || true
    echo "E2E environment stopped."

# View E2E container logs
[doc("View E2E logs: just e2e logs [backend|frontend|db|redis]")]
logs *SERVICE:
    #!/usr/bin/env bash
    if [ -z "{{ SERVICE }}" ]; then
        # Show all logs interleaved
        docker logs e2e-backend 2>&1 | sed 's/^/[backend] /' &
        docker logs e2e-frontend 2>&1 | sed 's/^/[frontend] /' &
        wait
    else
        docker logs -f "e2e-{{ SERVICE }}"
    fi

# =============================================================================
# Test
# =============================================================================

# Run E2E tests (tears down containers when done)
[doc("Run E2E tests (tears down containers when done)")]
test *ARGS:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "$(git rev-parse --show-toplevel)/e2e-tests"

    # Check if containers are running
    if ! docker ps --format '{{ '{{.Names}}' }}' | grep -q "^e2e-backend$"; then
        echo "E2E environment not running. Start with: just e2e up"
        exit 1
    fi

    # Ensure cleanup on exit (success or failure)
    cleanup() {
        echo ""
        echo "Tearing down E2E environment..."
        just down
    }
    trap cleanup EXIT

    export PUBLIC_BACKEND_URL="{{ public_backend_url }}"
    export PUBLIC_WEB_FRONTEND_URL="{{ public_frontend_url }}"
    export BASEROW_BUILDER_PREVIEW_URL="{{ builder_preview_url }}"
    export BASEROW_FRONTEND_COOKIE_PREFIX={{ frontend_cookie_prefix }}
    # The stub as the backend reaches it, on the compose network, and MailHog
    # as the tests reach it, from the host.
    export E2E_HTTP_STUB_URL=http://e2e-httpbin:80
    export E2E_SLACK_STUB=yes
    export E2E_MAIL_API_URL=http://localhost:{{ mail_api_port }}
    export E2E_BARRIER_STUB_URL=http://e2e-barrier:8080
    export E2E_BARRIER_CONTROL_URL=http://localhost:{{ barrier_control_port }}
    export E2E_BUTTON_RATE_LIMIT={{ button_rate_limit }}

    # Install dependencies if needed
    echo "Installing dependencies..."
    yarn install
    yarn playwright install chromium firefox

    echo "Waiting for services to be ready..."
    ./wait-for-services.sh

    echo "Running E2E tests..."
    if [ -z "{{ ARGS }}" ]; then
        yarn run test
    else
        yarn playwright test {{ ARGS }}
    fi

# Full E2E cycle: build, start, test, stop
[doc("Full E2E cycle: build, up, test, down")]
run: build up test down

# =============================================================================
# Database Dump
# =============================================================================

# Generate a fresh database dump with migrations applied (no templates)
# Templates are synced on E2E startup via post_migrate signal
[doc("Generate database dump with migrations only")]
db-dump:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "$(git rev-parse --show-toplevel)"

    echo "=== Creating fresh database dump for E2E tests ==="

    # Cleanup any existing containers
    docker rm -f {{ dump_db }} e2e-dump-backend e2e-dump-redis 2>/dev/null || true
    docker network rm {{ dump_network }} 2>/dev/null || true

    # Create network
    docker network create {{ dump_network }}

    # Start PostgreSQL with pgvector
    echo "Starting PostgreSQL..."
    docker run -d --network={{ dump_network }} --name {{ dump_db }} \
        -e POSTGRES_USER=baserow \
        -e POSTGRES_PASSWORD=baserow \
        -e POSTGRES_DB=baserow \
        {{ db_image }}

    # Wait for PostgreSQL
    echo "Waiting for PostgreSQL to be ready..."
    for i in {1..30}; do
        if docker exec {{ dump_db }} pg_isready -U baserow > /dev/null 2>&1; then
            break
        fi
        sleep 1
    done

    # Build backend image if needed
    if ! docker image inspect {{ backend_image }} > /dev/null 2>&1; then
        echo "Building backend CI image..."
        just build backend ci
    fi

    # Start Redis (needed for Django)
    echo "Starting Redis..."
    docker run -d --network={{ dump_network }} --name e2e-dump-redis {{ redis_image }}
    sleep 2

    # Run migrations only (use test settings which doesn't sync templates)
    echo "Running migrations..."
    docker run --rm --network={{ dump_network }} --name e2e-dump-backend \
        -e DATABASE_HOST={{ dump_db }} \
        -e DATABASE_NAME=baserow \
        -e DATABASE_USER=baserow \
        -e DATABASE_PASSWORD=baserow \
        -e REDIS_URL=redis://e2e-dump-redis:6379 \
        -e SECRET_KEY=dump-generation \
        -e DJANGO_SETTINGS_MODULE=baserow.config.settings.test \
        {{ backend_image }} bash -c "python /baserow/backend/src/baserow/manage.py migrate --noinput"

    # Create the dump
    echo "Creating database dump..."
    mkdir -p e2e-tests/fixtures
    docker exec {{ dump_db }} pg_dump -U baserow -Fc baserow > e2e-tests/{{ dump_file }}

    # Show dump info
    DUMP_SIZE=$(ls -lh e2e-tests/{{ dump_file }} | awk '{print $5}')
    echo ""
    echo "=== Database dump created successfully ==="
    echo "File: e2e-tests/{{ dump_file }}"
    echo "Size: $DUMP_SIZE"
    echo ""

    # Cleanup
    echo "Cleaning up..."
    docker rm -f {{ dump_db }} e2e-dump-redis 2>/dev/null || true
    docker network rm {{ dump_network }} 2>/dev/null || true

    echo "Done! Commit the dump file to the repository."

# Restore database from dump file to a container
[doc("Restore database dump to a PostgreSQL container")]
db-restore container:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "$(git rev-parse --show-toplevel)"

    DUMP_PATH="e2e-tests/{{ dump_file }}"

    if [ ! -f "$DUMP_PATH" ]; then
        echo "Error: Dump file not found at $DUMP_PATH"
        echo "Run 'just e2e db-dump' to generate it first."
        exit 1
    fi

    echo "Restoring database from dump..."
    docker exec {{ container }} psql -U baserow -d postgres -c "DROP DATABASE IF EXISTS baserow;"
    docker exec {{ container }} psql -U baserow -d postgres -c "CREATE DATABASE baserow;"
    docker exec -i {{ container }} pg_restore -U baserow -d baserow --no-owner --no-acl < "$DUMP_PATH" || true
    echo "Database restored successfully!"
