name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  merge_group:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  security:
    name: Security Scan
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Gitleaks
        if: github.actor != 'dependabot[bot]'
        uses: trails-cool/gitleaks-action@4cbc857b9cfa2a3297fe2be1078e196d30d1b424
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - name: Dependency audit
        run: pnpm audit --audit-level=high
        continue-on-error: true

  dockerfile-check:
    name: Dockerfile Package Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: bash scripts/check-dockerfiles.sh

  typecheck:
    name: Typecheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint

  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  e2e:
    name: E2E Tests
    needs: build
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: postgres://trails:trails@localhost:5432/trails
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile

      - name: Cache PostGIS Docker image
        id: postgis-cache
        uses: actions/cache@v5
        with:
          path: /tmp/postgis-image.tar
          key: postgis-16-3.4

      - name: Load or pull PostGIS image
        run: |
          if [ -f /tmp/postgis-image.tar ]; then
            docker load < /tmp/postgis-image.tar
          else
            docker pull postgis/postgis:16-3.4
            docker save postgis/postgis:16-3.4 > /tmp/postgis-image.tar
          fi

      - name: Start PostgreSQL
        run: |
          docker run -d --name postgres \
            -e POSTGRES_USER=trails \
            -e POSTGRES_PASSWORD=trails \
            -e POSTGRES_DB=trails \
            -p 5432:5432 \
            postgis/postgis:16-3.4
          # Wait for pg_isready
          for i in $(seq 1 30); do
            docker exec postgres pg_isready -U trails > /dev/null 2>&1 && break
            sleep 1
          done
          # Wait for PostGIS extension to be ready
          for i in $(seq 1 10); do
            docker exec postgres psql -U trails -c "SELECT PostGIS_Version();" > /dev/null 2>&1 && break
            sleep 1
          done

      - name: Push database schema
        run: pnpm db:push

      - name: Build and cache BRouter
        id: brouter-cache
        uses: actions/cache@v5
        with:
          path: /tmp/brouter
          key: brouter-1.7.8

      - name: Download BRouter
        if: steps.brouter-cache.outputs.cache-hit != 'true'
        run: |
          mkdir -p /tmp/brouter
          wget -q "https://github.com/abrensch/brouter/releases/download/v1.7.8/brouter-1.7.8.zip" -O /tmp/brouter/brouter.zip
          cd /tmp/brouter && unzip -o brouter.zip && mv brouter-1.7.8/* . && rmdir brouter-1.7.8 && rm brouter.zip

      - name: Cache BRouter segment
        id: segment-cache
        uses: actions/cache@v5
        with:
          path: /tmp/brouter-segments
          key: brouter-segment-E10_N50

      - name: Download Berlin segment
        if: steps.segment-cache.outputs.cache-hit != 'true'
        run: |
          mkdir -p /tmp/brouter-segments
          wget -q "https://brouter.de/brouter/segments4/E10_N50.rd5" -O /tmp/brouter-segments/E10_N50.rd5

      - name: Start BRouter
        run: |
          cd /tmp/brouter
          java -Xmx256M -Xms64M \
            -DmaxRunningTime=300 \
            -cp brouter-1.7.8-all.jar \
            btools.server.RouteServer \
            /tmp/brouter-segments profiles2 profiles2 \
            17777 2 &
          # Wait for BRouter to start
          for i in $(seq 1 30); do
            curl -sf http://localhost:17777/brouter?lonlats=13.4,52.5\|13.5,52.5\&profile=trekking\&format=geojson > /dev/null 2>&1 && break
            sleep 2
          done
        env:
          BROUTER_URL: http://localhost:17777

      - name: Cache Playwright browsers
        id: playwright-cache
        uses: actions/cache@v5
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ hashFiles('pnpm-lock.yaml') }}

      - name: Install Playwright
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: pnpm exec playwright install --with-deps chromium

      - name: Install Playwright deps only
        if: steps.playwright-cache.outputs.cache-hit == 'true'
        run: pnpm exec playwright install-deps chromium

      - name: Build for production
        run: pnpm build
        env:
          VITE_SENTRY_ENVIRONMENT: ci

      - name: Run E2E tests
        run: pnpm test:e2e
        env:
          BROUTER_URL: http://localhost:17777

      - name: Playwright job summary
        if: ${{ !cancelled() }}
        run: |
          if [ -f playwright-results.json ]; then
            node -e "
              const r = require('./playwright-results.json');
              const s = r.stats;
              const dur = (s.duration / 1000).toFixed(1);
              let md = '## Playwright E2E Results\n\n';
              md += '| Status | Count |\n|--------|-------|\n';
              md += '| :white_check_mark: Passed | ' + s.expected + ' |\n';
              if (s.unexpected > 0) md += '| :x: Failed | ' + s.unexpected + ' |\n';
              if (s.flaky > 0) md += '| :warning: Flaky | ' + s.flaky + ' |\n';
              if (s.skipped > 0) md += '| :fast_forward: Skipped | ' + s.skipped + ' |\n';
              md += '| :stopwatch: Duration | ' + dur + 's |\n\n';
              for (const file of r.suites) {
                for (const describe of (file.suites || [])) {
                  md += '### ' + describe.title + '\n\n';
                  for (const spec of (describe.specs || [])) {
                    const icon = spec.ok ? ':white_check_mark:' : ':x:';
                    const t = spec.tests?.[0]?.results?.[0]?.duration;
                    md += '- ' + icon + ' ' + spec.title + (t ? ' (' + t + 'ms)' : '') + '\n';
                  }
                  md += '\n';
                }
              }
              require('fs').appendFileSync(process.env.GITHUB_STEP_SUMMARY, md);
            "
          fi

      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
