name: Visual diff

# Same as preview-smoke — hits the homelab deployment at deckhearth.stillwell.cloud.
# Paths are tcg-vault-specific (pages router, JS).

on:
  pull_request:
    branches: [main]
    paths:
      - 'pages/**'
      # Exclude API-only edits — they don't render UI, so they can't move
      # any visual-diff pixels. Order matters: GitHub Actions evaluates the
      # `paths:` list with minimatch and applies `!`-prefixed exclusions
      # only after they've already matched a prior include. Keep this entry
      # immediately AFTER `pages/**`.
      # Surfaced by `tighten-visual-diff-path-filter` after PR #19
      # (cors-tighten) and PR #20 (add-rate-limiting) both falsely
      # triggered Screenshot diff at ~55s/PR.
      - '!pages/api/**'
      - 'components/**'
      - 'styles/**'
      - 'tailwind.config.js'
      - 'postcss.config.js'
      # Baseline-only PRs (specs + committed snapshots) should still run
      # visual diff now that home.png is in tests/visual/__screenshots__/.
      - 'tests/visual/**'

concurrency:
  group: visual-diff-${{ github.event.pull_request.number }}
  cancel-in-progress: true

permissions:
  contents: read
  pull-requests: write

jobs:
  gate:
    name: Should run?
    runs-on: [self-hosted, axiom]
    outputs:
      should_run: ${{ steps.check.outputs.should_run }}
    steps:
      - id: check
        env:
          # See preview-smoke.yml for the rationale: ${{ github.event.* }}
          # inlined into shell is a syntax-error + injection vector.
          PR_BODY: ${{ github.event.pull_request.body }}
          PR_IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
        run: |
          if [[ "$PR_IS_FORK" == "true" ]]; then
            echo "should_run=false" >> $GITHUB_OUTPUT
            echo "::notice::Visual diff skipped on fork PR (bypass secret unavailable to forks)"
          elif echo "$PR_BODY" | grep -qE 'pipeline:.*skip.*\bvisual\b'; then
            echo "should_run=false" >> $GITHUB_OUTPUT
            echo "::notice::Visual diff skipped via pipeline directive"
          else
            echo "should_run=true" >> $GITHUB_OUTPUT
          fi

  visual:
    name: Screenshot diff
    needs: gate
    if: needs.gate.outputs.should_run == 'true'
    runs-on: [self-hosted, axiom]
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4

      - name: Wait for homelab app health
        run: |
          BASE_URL="${SMOKE_BASE_URL:-https://deckhearth.stillwell.cloud}"
          for i in $(seq 1 30); do
            if curl -fsS "${BASE_URL}/api/health" >/dev/null; then
              echo "Homelab app healthy at ${BASE_URL}"
              exit 0
            fi
            echo "Waiting for ${BASE_URL}/api/health (${i}/30)…"
            sleep 10
          done
          echo "::error::Homelab app did not become healthy in time"
          exit 1
        env:
          SMOKE_BASE_URL: ${{ vars.SMOKE_BASE_URL }}

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm

      - name: Cache node_modules
        id: cache-node-modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}

      - run: npm ci
        if: steps.cache-node-modules.outputs.cache-hit != 'true'

      # Cache Playwright browsers — see preview-smoke.yml for rationale.
      - name: Resolve Playwright version
        id: pw-version
        run: |
          VERSION=$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
      - name: Cache Playwright browsers
        id: cache-playwright
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}-chromium

      - name: Install Playwright browsers
        run: |
          if [ "${{ steps.cache-playwright.outputs.cache-hit }}" = "true" ]; then
            npx playwright install-deps chromium
          else
            npx playwright install --with-deps chromium
          fi

      # Hard-gating after `harden-visual-diff-gate` brief 2 (PR #140).
      # The prior `continue-on-error: true` was a documented end state of
      # `adopt-playwright-smoke` (Decision 4) — necessary while no Linux
      # baseline existed. PR #58 seeded the first baseline, and PR #139
      # refreshed it against post-glass-redesign main. Visual diff is now
      # a real merge gate.
      #
      # If this step fails on a UI-touching PR, two paths:
      #   (a) Intentional change: dispatch
      #       `.github/workflows/seed-visual-baselines.yml` against this
      #       PR's preview URL, merge the resulting baseline-refresh PR,
      #       then re-run this workflow.
      #   (b) Unintentional regression: open the artifact bundle from
      #       this run, inspect the diff PNG, fix the regression in
      #       source, push.
      - name: Capture screenshots (PR)
        run: npx playwright test --project=visual --update-snapshots=none
        env:
          BASE_URL: ${{ vars.SMOKE_BASE_URL || 'https://deckhearth.stillwell.cloud' }}

      - name: Upload screenshots + diffs
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: visual-diff
          path: |
            tests/visual/__screenshots__/
            test-results/
          retention-days: 7

      - name: Comment on PR with diff link
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Visual Diff\n\nScreenshots and diffs uploaded as artifacts: [view run](${run})\n\nIf intentional changes: update snapshots locally with \`npx playwright test --project=visual --update-snapshots\` and commit.`
            });
