#!/bin/sh
set -eu

usage() {
    echo "usage: $0 [env-file]" >&2
    exit 64
}

[ "$#" -le 1 ] || usage

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
workspace_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
registry_file="$script_dir/conditional-integration-registry.tsv"
env_file=${1:-"$workspace_root/.env.ci"}

if [ ! -f "$env_file" ]; then
    echo "missing integration environment file: $env_file" >&2
    exit 66
fi
if [ ! -f "$registry_file" ]; then
    echo "missing integration marker registry: $registry_file" >&2
    exit 66
fi
env_file=$(CDPATH= cd -- "$(dirname -- "$env_file")" && pwd)/$(basename -- "$env_file")

# .env.ci is generated by the sam_rebuild development Compose helper and is a
# shell-compatible, credential-bearing local file. Never print its contents.
set -a
# shellcheck disable=SC1090
. "$env_file"
set +a

: "${POSTGRES_HOST:?POSTGRES_HOST is required in $env_file}"
: "${POSTGRES_PORT:?POSTGRES_PORT is required in $env_file}"
: "${POSTGRES_DB:?POSTGRES_DB is required in $env_file}"
: "${POSTGRES_USER:?POSTGRES_USER is required in $env_file}"
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required in $env_file}"
: "${REDIS_URL:?REDIS_URL is required in $env_file}"

node_tag=$(printf '%s' "${CI_NODE_INDEX:-local}" | tr -cd 'a-zA-Z0-9_' | tr 'A-Z' 'a-z' | cut -c1-8)
run_id=$(date -u +%m%d%H%M%S)_$$_${node_tag}
export CONDITIONAL_INTEGRATION_RUN_ID=$run_id
schema_ownership_token="sammo-conditional-integration:$run_id"
supported_registry_modes="core create_general external_fixture gateway_runtime immediate_action npc_possession read_model_journal reference_live_sortie reference_npc_possession select_pool"
term_grace_seconds=${CONDITIONAL_INTEGRATION_TERM_GRACE_SECONDS:-10}
case "$term_grace_seconds" in
    ''|*[!0-9]*)
        echo "CONDITIONAL_INTEGRATION_TERM_GRACE_SECONDS must be an integer from 1 to 60" >&2
        exit 64
        ;;
esac
if [ "$term_grace_seconds" -lt 1 ] || [ "$term_grace_seconds" -gt 60 ]; then
    echo "CONDITIONAL_INTEGRATION_TERM_GRACE_SECONDS must be an integer from 1 to 60" >&2
    exit 64
fi
integration_schema=${CONDITIONAL_INTEGRATION_SCHEMA:-ci_${run_id}_integration}
scenario_schema=${SCENARIO_SEED_INTEGRATION_SCHEMA:-ci_${run_id}_scenario_seed}
npc_possession_schema=${NPC_POSSESSION_INTEGRATION_SCHEMA:-ci_${run_id}_npc_possession_integration}
create_general_schema=${CREATE_GENERAL_INTEGRATION_SCHEMA:-ci_${run_id}_create_general_integration}
select_pool_schema=${SELECT_POOL_INTEGRATION_SCHEMA:-ci_${run_id}_select_pool_integration}
immediate_action_schema=${IMMEDIATE_ACTION_INTEGRATION_SCHEMA:-ci_${run_id}_immediate_action_integration}
gateway_runtime_schema=${GATEWAY_RUNTIME_INTEGRATION_SCHEMA:-ci_${run_id}_gateway_runtime_integration}
read_model_journal_schema=${READ_MODEL_JOURNAL_INTEGRATION_SCHEMA:-ci_${run_id}_read_model_journal_integration}
npc_possession_differential_schema=${NPC_POSSESSION_DIFFERENTIAL_SCHEMA:-ci_${run_id}_npc_possession_differential}
live_sortie_schema=${LIVE_SORTIE_PERSISTENCE_SCHEMA:-ci_${run_id}_live_sortie_persistence}

for schema in \
    "$integration_schema" \
    "$scenario_schema" \
    "$npc_possession_schema" \
    "$create_general_schema" \
    "$select_pool_schema" \
    "$immediate_action_schema" \
    "$gateway_runtime_schema" \
    "$read_model_journal_schema" \
    "$npc_possession_differential_schema" \
    "$live_sortie_schema"; do
    case "$schema" in
        ''|[!a-z_]*|*[!a-z0-9_]*)
            echo "integration schema must be a lowercase PostgreSQL identifier: $schema" >&2
            exit 64
            ;;
    esac
    if [ "${#schema}" -gt 63 ]; then
        echo "integration schema exceeds PostgreSQL's 63-byte identifier limit: $schema" >&2
        exit 64
    fi
done

report_dir=$(mktemp -d)
summary_file="$report_dir/summary.tsv"
validated_registry_file="$report_dir/validated-registry.tsv"
image_upload_secret_file="$report_dir/image-upload-secret"
(
    umask 077
    node --input-type=module -e \
        'import { randomBytes } from "node:crypto"; process.stdout.write(randomBytes(32).toString("hex"));' \
        >"$image_upload_secret_file"
)
export GAME_IMAGE_UPLOAD_SECRET_FILE=$image_upload_secret_file
export GATEWAY_IMAGE_UPLOAD_SECRET_FILE=$image_upload_secret_file
active_process_group=
cleanup_resources_started=0

build_database_url() {
    SCHEMA_NAME=$1 node --input-type=module -e '
        const required = [
            "POSTGRES_HOST",
            "POSTGRES_PORT",
            "POSTGRES_DB",
            "POSTGRES_USER",
            "POSTGRES_PASSWORD",
            "SCHEMA_NAME",
        ];
        for (const name of required) {
            if (!process.env[name]) {
                throw new Error(`missing ${name}`);
            }
        }
        const url = new URL("postgresql://localhost");
        url.hostname = process.env.POSTGRES_HOST;
        url.port = process.env.POSTGRES_PORT;
        url.username = process.env.POSTGRES_USER;
        url.password = process.env.POSTGRES_PASSWORD;
        url.pathname = `/${process.env.POSTGRES_DB}`;
        url.searchParams.set("schema", process.env.SCHEMA_NAME);
        process.stdout.write(url.href);
    '
}

base_database_url=$(build_database_url public)

create_owned_schema() {
    schema=$1
    SCHEMA_NAME=$schema SCHEMA_OWNERSHIP_TOKEN=$schema_ownership_token \
        DATABASE_URL=$base_database_url \
        pnpm --filter @sammo-ts/infra exec node --input-type=module -e '
            import pg from "pg";
            const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
            const quoteIdentifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
            const apostrophe = String.fromCharCode(39);
            const quoteLiteral = (value) =>
                `${apostrophe}${value.replaceAll(apostrophe, apostrophe.repeat(2))}${apostrophe}`;
            await client.connect();
            try {
                await client.query("BEGIN");
                await client.query(`CREATE SCHEMA ${quoteIdentifier(process.env.SCHEMA_NAME)}`);
                await client.query(
                    `COMMENT ON SCHEMA ${quoteIdentifier(process.env.SCHEMA_NAME)} IS ${quoteLiteral(
                        process.env.SCHEMA_OWNERSHIP_TOKEN
                    )}`
                );
                await client.query("COMMIT");
            } catch (error) {
                await client.query("ROLLBACK");
                throw error;
            } finally {
                await client.end();
            }
        '
}

drop_owned_schemas() {
    SCHEMA_OWNERSHIP_TOKEN=$schema_ownership_token DATABASE_URL=$base_database_url \
        pnpm --filter @sammo-ts/infra exec node --input-type=module -e '
            import pg from "pg";
            const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
            const quoteIdentifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
            await client.connect();
            try {
                const result = await client.query(
                    `SELECT nspname
                     FROM pg_namespace
                     WHERE obj_description(oid, $$pg_namespace$$) = $1
                     ORDER BY nspname`,
                    [process.env.SCHEMA_OWNERSHIP_TOKEN]
                );
                for (const { nspname } of result.rows) {
                    await client.query(`DROP SCHEMA ${quoteIdentifier(nspname)} CASCADE`);
                }
            } finally {
                await client.end();
            }
        ' >/dev/null
}

delete_owned_redis_keys() {
    CONDITIONAL_INTEGRATION_RUN_ID=$run_id REDIS_URL=$REDIS_URL \
        pnpm --filter @sammo-ts/infra exec node --input-type=module -e '
            import { createClient } from "redis";
            const client = createClient({ url: process.env.REDIS_URL });
            const runId = process.env.CONDITIONAL_INTEGRATION_RUN_ID;
            const patterns = [
                `sammo:game:*:che:security-http-${runId}:*`,
                `sammo:game:*:che:nation-html-${runId}:*`,
                `sammo:che:battle-sim-e2e-${runId}-*:battle-sim:*`,
            ];
            await client.connect();
            try {
                for (const pattern of patterns) {
                    for await (const keys of client.scanIterator({ MATCH: pattern, COUNT: 100 })) {
                        if (keys.length > 0) {
                            await client.del(keys);
                        }
                    }
                }
            } finally {
                await client.quit();
            }
        ' >/dev/null
}

terminate_active_process_group() {
    [ -n "$active_process_group" ] || return 0
    process_group=$active_process_group
    kill -TERM "-$process_group" 2>/dev/null || true
    waited_seconds=0
    while kill -0 "-$process_group" 2>/dev/null &&
        [ "$waited_seconds" -lt "$term_grace_seconds" ]; do
        sleep 1
        waited_seconds=$((waited_seconds + 1))
    done
    if kill -0 "-$process_group" 2>/dev/null; then
        echo "conditional integration process group did not stop after ${term_grace_seconds}s; sending SIGKILL" >&2
        kill -KILL "-$process_group" 2>/dev/null || true
    fi
    wait "$process_group" 2>/dev/null || true
    active_process_group=
}

print_summary() {
    if [ ! -s "$summary_file" ]; then
        echo "conditional integration summary: 0 passed files, 0 skipped files, 0 failed files"
        return
    fi
    awk -F '\t' '
        {
            passed += $2;
            skipped += $3;
            failed += $4;
        }
        END {
            printf "conditional integration summary: %d passed files, %d skipped files, %d failed files\n",
                passed, skipped, failed;
        }
    ' "$summary_file"
}

handle_exit() {
    exit_status=$?
    trap - EXIT HUP INT TERM
    terminate_active_process_group
    if [ "$cleanup_resources_started" -eq 1 ]; then
        if ! drop_owned_schemas && [ "$exit_status" -eq 0 ]; then
            exit_status=1
        fi
        if ! delete_owned_redis_keys && [ "$exit_status" -eq 0 ]; then
            exit_status=1
        fi
    fi
    print_summary
    rm -rf "$report_dir"
    exit "$exit_status"
}

trap handle_exit EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

validate_marker_registry() {
    discovered_file="$report_dir/discovered-markers.txt"
    registered_file="$report_dir/registered-markers.txt"
    registry_errors_file="$report_dir/registry-errors.txt"
    missing_file="$report_dir/unregistered-markers.txt"
    stale_file="$report_dir/stale-markers.txt"

    (
        cd "$workspace_root"
        rg -o --no-filename \
            'process\.env\.[A-Z0-9_]+_DATABASE_URL' \
            app/gateway-api/test \
            app/game-api/test \
            app/game-engine/test \
            tools/integration-tests/test \
            -g '*.integration.test.ts' |
            sed 's/process\.env\.//' |
            sort -u
    ) >"$discovered_file"
    : >"$validated_registry_file"
    awk -F '\t' -v supported_modes="$supported_registry_modes" \
        -v validated_registry_file="$validated_registry_file" '
        BEGIN {
            split(supported_modes, mode_names, " ");
            for (mode_index in mode_names) {
                allowed_modes[mode_names[mode_index]] = 1;
            }
        }
        /^#/ || /^[[:space:]]*$/ { next }
        NF != 2 {
            printf "line %d must contain exactly one tab-separated marker and mode\n", NR;
            failed = 1;
            next;
        }
        $1 !~ /^[A-Z][A-Z0-9_]*_DATABASE_URL$/ {
            printf "line %d has invalid marker: %s\n", NR, $1;
            failed = 1;
        }
        !($2 in allowed_modes) {
            printf "line %d has unsupported execution mode: %s\n", NR, $2;
            failed = 1;
        }
        ($2 in allowed_modes) {
            mode_counts[$2]++;
        }
        seen[$1]++ {
            printf "line %d duplicates marker: %s\n", NR, $1;
            failed = 1;
        }
        {
            print $1 "\t" $2 > validated_registry_file;
        }
        END {
            for (mode_name in allowed_modes) {
                if (!mode_counts[mode_name]) {
                    printf "supported execution mode has no marker: %s\n", mode_name;
                    failed = 1;
                }
            }
            exit failed;
        }
    ' "$registry_file" >"$registry_errors_file" || {
        echo "invalid conditional integration marker registry:" >&2
        sed 's/^/  /' "$registry_errors_file" >&2
        exit 65
    }
    sort -u "$validated_registry_file" -o "$validated_registry_file"
    cut -f1 "$validated_registry_file" >"$registered_file"

    comm -23 "$discovered_file" "$registered_file" >"$missing_file"
    if [ -s "$missing_file" ]; then
        echo "unregistered conditional database marker(s):" >&2
        sed 's/^/  /' "$missing_file" >&2
        exit 65
    fi

    comm -13 "$discovered_file" "$registered_file" >"$stale_file"
    if [ -s "$stale_file" ]; then
        echo "stale conditional database marker(s):" >&2
        sed 's/^/  /' "$stale_file" >&2
        exit 65
    fi
}

markers_for_mode() {
    mode=$1
    awk -F '\t' -v mode="$mode" '$2 == mode { print $1 }' "$validated_registry_file" |
        paste -sd '|' -
}

record_vitest_result() {
    label=$1
    result_file=$2
    counts=$(node --input-type=module -e '
        import fs from "node:fs";
        const result = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
        const files = Array.isArray(result.testResults) ? result.testResults : [];
        const isSkipped = ({ assertionResults = [], status }) =>
            status === "pending" ||
            (assertionResults.length > 0 &&
                assertionResults.every(({ status: assertionStatus }) =>
                    assertionStatus === "pending" || assertionStatus === "todo"
                ));
        const skipped = files.filter(isSkipped).length;
        const failed = files.filter(({ status }) => status === "failed").length;
        const passed = files.length - skipped - failed;
        process.stdout.write(`${passed}\t${skipped}\t${failed}`);
    ' "$result_file")
    printf '%s\t%s\n' "$label" "$counts" >>"$summary_file"
    skipped=$(printf '%s' "$counts" | cut -f2)
    if [ "$skipped" -ne 0 ]; then
        echo "$label left $skipped integration test file(s) skipped" >&2
        return 1
    fi
}

run_vitest() {
    package_dir=$1
    label=$2
    shift 2
    result_file="$report_dir/$(printf '%s' "$label" | tr -cd 'a-zA-Z0-9_' | tr 'A-Z' 'a-z').json"

    echo "running $label tests in $package_dir"
    (
        cd "$workspace_root/$package_dir"
        exec setsid pnpm exec vitest run \
            --no-file-parallelism \
            --maxWorkers=1 \
            --reporter=default \
            --reporter=json \
            --outputFile.json="$result_file" \
            "$@"
    ) &
    active_process_group=$!
    test_status=0
    wait "$active_process_group" || test_status=$?
    active_process_group=

    result_status=0
    if [ -f "$result_file" ]; then
        record_vitest_result "$label" "$result_file" || result_status=$?
    else
        printf '%s\t0\t0\t1\n' "$label" >>"$summary_file"
        result_status=1
    fi
    if [ "$test_status" -eq 0 ] && [ "$result_status" -ne 0 ]; then
        test_status=$result_status
    fi
    return "$test_status"
}

run_marked_tests() {
    package_dir=$1
    marker=$2
    label=$3
    if [ -z "$marker" ]; then
        echo "no conditional database marker configured for $label" >&2
        exit 65
    fi
    test_files=$(cd "$workspace_root/$package_dir" && rg -l "$marker" test -g '*.integration.test.ts' | sort)
    if [ -z "$test_files" ]; then
        echo "no $label tests found under $package_dir" >&2
        exit 65
    fi
    # Integration test paths cannot contain whitespace in this repository.
    # shellcheck disable=SC2086
    run_vitest "$package_dir" "$label" $test_files
}

run_redis_only_tests() {
    package_dir=app/game-api
    database_marker=$1
    test_files=$(
        cd "$workspace_root/$package_dir"
        redis_files=$(rg -l 'process\.env\.REDIS_URL' test -g '*.integration.test.ts' | sort)
        # Files already selected through a database marker run once in that
        # database group, where Redis is also available.
        # shellcheck disable=SC2086
        rg --files-without-match "$database_marker" $redis_files
    )
    if [ -z "$test_files" ]; then
        echo "no Redis-only integration tests found under $package_dir" >&2
        exit 65
    fi
    # shellcheck disable=SC2086
    run_vitest "$package_dir" "game_api_redis" $test_files
}

export PATH
cd "$workspace_root"
validate_marker_registry

pnpm install --frozen-lockfile
pnpm --filter @sammo-ts/infra prisma:generate
pnpm --filter @sammo-ts/common build
pnpm --filter @sammo-ts/logic build
pnpm --filter @sammo-ts/infra build
pnpm --filter @sammo-ts/game-engine build

GATEWAY_MIGRATION_TEST_DATABASE_URL=$base_database_url \
    pnpm --filter @sammo-ts/infra verify:migration:account-icon

cleanup_resources_started=1
create_owned_schema "$integration_schema"
create_owned_schema "$npc_possession_schema"
create_owned_schema "$scenario_schema"

database_url=$(build_database_url "$integration_schema")
export POSTGRES_SCHEMA=$integration_schema
export DATABASE_URL=$database_url
export INPUT_EVENT_DATABASE_URL=$database_url
export GENERAL_LIFECYCLE_DATABASE_URL=$database_url
export TURN_DAEMON_LEASE_DATABASE_URL=$database_url
export TURN_DIFFERENTIAL_DATABASE_URL=$database_url
export RESERVED_TURN_DATABASE_URL=$database_url
export PROFILE_SEED_CLI_DATABASE_URL=$database_url
export PROFILE_SEED_DATABASE_URL=$database_url
profile_lock_secondary_database_url=$(build_database_url "$scenario_schema")
export PROFILE_LOCK_SECONDARY_DATABASE_URL=$profile_lock_secondary_database_url

# The infra PostgreSQL boundary tests assert migration-owned seed rows, CHECK
# constraints, and indexes. `prisma db push` only materializes the Prisma data
# model, so provision the primary integration schema through the production
# migration chain.
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game

core_database_markers=$(markers_for_mode core)
run_marked_tests app/game-api "$core_database_markers" "game_api_postgresql"
run_marked_tests app/game-engine "$core_database_markers" "game_engine_postgresql"
run_marked_tests tools/integration-tests "$core_database_markers" "snapshot_postgresql"

create_owned_schema "$read_model_journal_schema"
read_model_journal_database_url=$(build_database_url "$read_model_journal_schema")
(
    export POSTGRES_SCHEMA=$read_model_journal_schema
    export DATABASE_URL=$read_model_journal_database_url
    pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
)
export READ_MODEL_JOURNAL_DATABASE_URL=$read_model_journal_database_url
run_marked_tests packages/infra \
    "$(markers_for_mode read_model_journal)" \
    "read_model_journal_infra_postgresql"
run_marked_tests app/game-engine \
    "$(markers_for_mode read_model_journal)" \
    "read_model_journal_engine_postgresql"

create_owned_schema "$create_general_schema"
create_general_database_url=$(build_database_url "$create_general_schema")
(
    export POSTGRES_SCHEMA=$create_general_schema
    export DATABASE_URL=$create_general_database_url
    pnpm --filter @sammo-ts/infra prisma:db:push:game
)
export CREATE_GENERAL_DATABASE_URL=$create_general_database_url
run_marked_tests app/game-api \
    "$(markers_for_mode create_general)" \
    "create_general_postgresql"

create_owned_schema "$select_pool_schema"
select_pool_database_url=$(build_database_url "$select_pool_schema")
(
    export POSTGRES_SCHEMA=$select_pool_schema
    export DATABASE_URL=$select_pool_database_url
    pnpm --filter @sammo-ts/infra prisma:db:push:game
)
export SELECT_POOL_DATABASE_URL=$select_pool_database_url
run_marked_tests app/game-api \
    "$(markers_for_mode select_pool)" \
    "select_pool_api_postgresql"
run_marked_tests app/game-engine \
    "$(markers_for_mode select_pool)" \
    "select_pool_engine_postgresql"

create_owned_schema "$immediate_action_schema"
immediate_action_database_url=$(build_database_url "$immediate_action_schema")
(
    export POSTGRES_SCHEMA=$immediate_action_schema
    export DATABASE_URL=$immediate_action_database_url
    pnpm --filter @sammo-ts/infra prisma:db:push:game
)
export IMMEDIATE_ACTION_DATABASE_URL=$immediate_action_database_url
run_marked_tests app/game-api \
    "$(markers_for_mode immediate_action)" \
    "immediate_action_api_postgresql"
run_marked_tests app/game-engine \
    "$(markers_for_mode immediate_action)" \
    "immediate_action_postgresql"

create_owned_schema "$gateway_runtime_schema"
gateway_runtime_database_url=$(build_database_url "$gateway_runtime_schema")
(
    export POSTGRES_SCHEMA=$gateway_runtime_schema
    export DATABASE_URL=$gateway_runtime_database_url
    export GATEWAY_DATABASE_URL=$gateway_runtime_database_url
    pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
    pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
)
export GATEWAY_RUNTIME_ACTION_DATABASE_URL=$gateway_runtime_database_url
export GATEWAY_OPERATION_DATABASE_URL=$gateway_runtime_database_url
export GATEWAY_RELEASE_DATABASE_URL=$gateway_runtime_database_url
export GATEWAY_RUNTIME_INTEGRATION_SCHEMA=$gateway_runtime_schema
run_marked_tests app/gateway-api \
    "$(markers_for_mode gateway_runtime)" \
    "gateway_runtime_gateway_api_postgresql"
run_marked_tests app/game-engine \
    "$(markers_for_mode gateway_runtime)" \
    "gateway_runtime_postgresql"

npc_possession_database_url=$(build_database_url "$npc_possession_schema")
(
    export POSTGRES_SCHEMA=$npc_possession_schema
    export DATABASE_URL=$npc_possession_database_url
    pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
    pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
)
export NPC_POSSESSION_DATABASE_URL=$npc_possession_database_url
run_marked_tests app/game-api \
    "$(markers_for_mode npc_possession)" \
    "npc_possession_postgresql"

if [ "${TURN_DIFFERENTIAL_REFERENCE:-}" = "1" ]; then
    reference_workspace_root=${TURN_DIFFERENTIAL_WORKSPACE_ROOT:-"$workspace_root/.."}
    reference_stack=${TURN_DIFFERENTIAL_STACK_DIR:-"$reference_workspace_root/docker_compose_files/reference"}
    if [ ! -f "$reference_stack/compose.yml" ]; then
        echo "reference compose file is missing; set TURN_DIFFERENTIAL_WORKSPACE_ROOT or TURN_DIFFERENTIAL_STACK_DIR" >&2
        exit 69
    fi
    if [ ! -f "$reference_workspace_root/ref/sam/d_setting/RootDB.php" ] ||
        [ ! -f "$reference_workspace_root/ref/sam/hwe/d_setting/DB.php" ]; then
        echo "reference runtime database configuration is missing under $reference_workspace_root/ref/sam" >&2
        exit 69
    fi
    if ! docker compose -f "$reference_stack/compose.yml" ps --status running --services | grep -qx db ||
        ! docker compose -f "$reference_stack/compose.yml" exec -T db healthcheck.sh --connect --innodb_initialized \
            >/dev/null 2>&1; then
        echo "TURN_DIFFERENTIAL_REFERENCE=1 requires a healthy bootstrapped reference db stack" >&2
        exit 69
    fi
    reference_tables=$(
        docker compose -f "$reference_stack/compose.yml" exec -T db sh -c '
            export MYSQL_PWD="$(cat /run/secrets/db_root_password)"
            mariadb --batch --skip-column-names -u root -e "
                SELECT COUNT(*) FROM information_schema.TABLES
                WHERE TABLE_SCHEMA = '\''$MARIADB_DATABASE'\'' AND TABLE_NAME = '\''member'\'';
                SELECT COUNT(*) FROM information_schema.TABLES
                WHERE TABLE_SCHEMA = '\''$REF_HWE_DB_NAME'\'' AND TABLE_NAME IN ('\''general'\'', '\''select_npc_token'\'');
            "
        ' 2>/dev/null
    ) || {
        echo "failed to inspect reference database bootstrap state" >&2
        exit 69
    }
    if [ "$reference_tables" != "$(printf '1\n2')" ]; then
        echo "reference root/HWE databases are not bootstrapped" >&2
        exit 69
    fi

    create_owned_schema "$npc_possession_differential_schema"
    npc_possession_differential_database_url=$(build_database_url "$npc_possession_differential_schema")
    (
        export POSTGRES_SCHEMA=$npc_possession_differential_schema
        export DATABASE_URL=$npc_possession_differential_database_url
        export NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL=$npc_possession_differential_database_url
        pnpm --filter @sammo-ts/infra prisma:db:push:game
    )
    export NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL=$npc_possession_differential_database_url
    export TURN_DIFFERENTIAL_WORKSPACE_ROOT=$reference_workspace_root
    export TURN_DIFFERENTIAL_STACK_DIR=$reference_stack
    run_marked_tests tools/integration-tests \
        "$(markers_for_mode reference_npc_possession)" \
        "npc_possession_reference"

    create_owned_schema "$live_sortie_schema"
    live_sortie_database_url=$(build_database_url "$live_sortie_schema")
    (
        export POSTGRES_SCHEMA=$live_sortie_schema
        export DATABASE_URL=$live_sortie_database_url
        pnpm --filter @sammo-ts/infra prisma:db:push:game
    )
    export POSTGRES_SCHEMA=$live_sortie_schema
    export DATABASE_URL=$live_sortie_database_url
    export LIVE_SORTIE_PERSISTENCE_DATABASE_URL=$live_sortie_database_url
    run_marked_tests tools/integration-tests \
        "$(markers_for_mode reference_live_sortie)" \
        "live_sortie_postgresql"
    export POSTGRES_SCHEMA=$integration_schema
    export DATABASE_URL=$database_url
fi

run_redis_only_tests "$core_database_markers"

scenario_database_url=$(build_database_url "$scenario_schema")
export POSTGRES_SCHEMA=$scenario_schema
export DATABASE_URL=$scenario_database_url
pnpm --filter @sammo-ts/infra prisma:db:push:game
run_vitest app/game-engine "scenario_seed_postgresql" test/scenarioSeeder.test.ts

echo "conditional PostgreSQL and Redis integration tests passed"
