set shell := ["bash", "-c"]

# How many jobs the parallel gate runs at once. Lower this when another
# worktree stack is active, or use 1 for a serial debugging run.
jobs := env('JUST_JOBS', '4')

default:
    @just --list

# Full completion gate. Every check runs in one bounded pool, with likely
# long Cypress groups scheduled first and shorter jobs filling free lanes.
# Start the worktree stack before running it because Cypress needs Vite.
test-all:
    just par "gate" {{ jobs }} \
        frontend-cypress-account frontend-cypress-session \
        frontend-cypress-sets backend-types-check \
        frontend-cypress-scheduling frontend-build \
        backend-test-feature frontend-cypress-today \
        frontend-lint-check frontend-type-check backend-test-unit \
        frontend-format-check cypress-spec-coverage

# Run recipes concurrently, buffering each one's output to its own log.
# Report jobs as they finish, then print every failed job's complete log.
[private]
par label max +targets:
    #!/usr/bin/env bash
    set -uo pipefail

    targets=({{ targets }})
    logs=$(mktemp -d)
    declare -A target_of started_at
    failed=()
    next=0
    running=0

    echo "==> {{ label }} (${#targets[@]} jobs, up to {{ max }} at a time)"

    while (( next < ${#targets[@]} || running > 0 )); do
      while (( next < ${#targets[@]} && running < {{ max }} )); do
        target="${targets[next]}"
        just "$target" > "$logs/$target.log" 2>&1 &
        target_of[$!]="$target"
        started_at[$!]=$SECONDS
        next=$(( next + 1 ))
        running=$(( running + 1 ))
      done

      wait -n -p finished
      status=$?
      running=$(( running - 1 ))
      target="${target_of[$finished]}"
      elapsed=$(( SECONDS - started_at[$finished] ))

      if (( status == 0 )); then
        printf '  ok   %-28s %4ds\n' "$target" "$elapsed"
      else
        printf '  FAIL %-28s %4ds\n' "$target" "$elapsed"
        failed+=("$target")
      fi
    done

    if (( ${#failed[@]} == 0 )); then
      rm -rf "$logs"
      exit 0
    fi

    for target in "${failed[@]}"; do
      printf '\n--- %s ---\n' "$target"
      cat "$logs/$target.log"
    done
    printf '\n{{ label }} failed: %s\n' "${failed[*]}"
    printf 'logs kept in %s\n' "$logs"
    exit 1

# Backend

backend-test *args:
    cd backend && php artisan test {{ args }}

backend-test-unit *args:
    cd backend && php artisan test --testsuite=Unit {{ args }}

backend-test-feature *args:
    cd backend && php artisan test --testsuite=Feature {{ args }}

backend-types-check:
    cd backend && composer types:check

fresh:
    cd backend && php artisan migrate:fresh --seed

# Frontend

frontend-format:
    cd frontend/website && npm run format

frontend-format-check:
    cd frontend/website && npm run format:check

frontend-lint:
    cd frontend/website && npm run lint

frontend-lint-check:
    cd frontend/website && npm run lint:check

frontend-type-check:
    cd frontend/website && npm run type-check

frontend-build:
    cd frontend/website && npm run build-only

frontend-cypress-run:
    cd frontend/website && npm run test:e2e

# Each Cypress spec must belong to exactly one feature group. The complete
# gate runs these groups concurrently; the whole-suite recipe above remains
# available for focused iteration.
frontend_specs_account := "confirm-email login signup"
frontend_specs_session := "guest-auth session-auth"
frontend_specs_sets := "set-layout sets-dashboard"
frontend_specs_scheduling := "set-scheduling"
frontend_specs_today := "today-assignments"

frontend-cypress-account:
    just _cypress {{ frontend_specs_account }}

frontend-cypress-session:
    just _cypress {{ frontend_specs_session }}

frontend-cypress-sets:
    just _cypress {{ frontend_specs_sets }}

frontend-cypress-scheduling:
    just _cypress {{ frontend_specs_scheduling }}

frontend-cypress-today:
    just _cypress {{ frontend_specs_today }}

[private]
_cypress +names:
    cd frontend/website && specs=$(for name in {{ names }}; do \
        printf 'cypress/e2e/%s.cy.ts,' "$name"; done) && \
        npm run test:e2e -- --spec "${specs%,}"

cypress-spec-coverage:
    just _spec-coverage {{ frontend_specs_account }} \
        {{ frontend_specs_session }} {{ frontend_specs_sets }} \
        {{ frontend_specs_scheduling }} {{ frontend_specs_today }}

[private]
_spec-coverage +names:
    #!/usr/bin/env bash
    set -uo pipefail

    cd frontend/website
    listed=$(printf '%s\n' {{ names }} | sort)
    on_disk=$(ls cypress/e2e/*.cy.ts | xargs -n1 basename \
        | sed 's/\.cy\.ts$//' | sort)

    duplicated=$(echo "$listed" | uniq -d)
    unique=$(echo "$listed" | uniq)
    ungrouped=$(comm -13 <(echo "$unique") <(echo "$on_disk"))
    missing=$(comm -23 <(echo "$unique") <(echo "$on_disk"))
    status=0

    if [ -n "$ungrouped" ]; then
      echo "in no group, so never runs in the gate:"
      printf '  %s\n' $ungrouped
      status=1
    fi

    if [ -n "$missing" ]; then
      echo "listed in a group but not on disk:"
      printf '  %s\n' $missing
      status=1
    fi

    if [ -n "$duplicated" ]; then
      echo "in more than one group, so runs twice:"
      printf '  %s\n' $duplicated
      status=1
    fi

    exit $status
