name: Continuous Fuzzing

on:
  # Run on main branch pushes
  push:
    branches: [main]
  # Run nightly
  schedule:
    - cron: '0 2 * * *'  # 2 AM UTC daily
  # Allow manual trigger
  workflow_dispatch:
    inputs:
      duration:
        description: 'Fuzz duration per target (seconds)'
        required: false
        default: '3600'

env:
  CARGO_TERM_COLOR: always

permissions:
  contents: read
  issues: write  # To create issues on fuzzing failures

jobs:
  # Discover all fuzz targets dynamically
  discover-targets:
    name: Discover Fuzz Targets
    runs-on: ubuntu-latest
    outputs:
      targets: ${{ steps.list-targets.outputs.targets }}
    steps:
      - uses: actions/checkout@v6

      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: nightly

      - name: Install cargo-fuzz
        run: cargo install cargo-fuzz

      - name: List fuzz targets
        id: list-targets
        run: |
          cd fuzz
          # Get all fuzz targets and format as JSON array
          TARGETS=$(cargo fuzz list | jq -R -s -c 'split("\n") | map(select(length > 0))')
          echo "targets=$TARGETS" >> $GITHUB_OUTPUT
          echo "Found targets:"
          echo "$TARGETS" | jq -r '.[]'

  # Long-running fuzz tests
  fuzz-continuous:
    name: Fuzz ${{ matrix.target }}
    runs-on: ubuntu-latest
    needs: discover-targets
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix:
        target: ${{ fromJson(needs.discover-targets.outputs.targets) }}
    steps:
      - uses: actions/checkout@v6

      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: nightly

      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: "fuzz -> target"

      - name: Install cargo-fuzz
        run: cargo install cargo-fuzz

      - name: Restore corpus
        uses: actions/cache@v5
        with:
          path: fuzz/corpus/${{ matrix.target }}
          key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }}
          restore-keys: |
            fuzz-corpus-${{ matrix.target }}-

      - name: Run fuzz target
        id: fuzz
        continue-on-error: true
        run: |
          cd fuzz
          DURATION=${{ github.event.inputs.duration || '3600' }}
          echo "Running ${{ matrix.target }} for ${DURATION} seconds"
          cargo fuzz run ${{ matrix.target }} -- \
            -max_total_time=${DURATION} \
            -print_final_stats=1 \
            -print_corpus_stats=1 \
            -rss_limit_mb=4096
        env:
          RUST_BACKTRACE: full

      - name: Minimize corpus
        if: always()
        run: |
          cd fuzz
          cargo fuzz cmin ${{ matrix.target }}

      - name: Save corpus
        if: always()
        uses: actions/cache/save@v5
        with:
          path: fuzz/corpus/${{ matrix.target }}
          key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }}

      - name: Check for crashes
        id: check_crashes
        if: always()
        run: |
          if [ -d "fuzz/artifacts/${{ matrix.target }}" ] && [ "$(ls -A fuzz/artifacts/${{ matrix.target }})" ]; then
            echo "crashes_found=true" >> $GITHUB_OUTPUT
            echo "## Crashes found in ${{ matrix.target }}" >> $GITHUB_STEP_SUMMARY
            ls -lh fuzz/artifacts/${{ matrix.target }}/ >> $GITHUB_STEP_SUMMARY
          else
            echo "crashes_found=false" >> $GITHUB_OUTPUT
            echo "✅ No crashes found in ${{ matrix.target }}" >> $GITHUB_STEP_SUMMARY
          fi

      - name: Upload crash artifacts
        if: steps.check_crashes.outputs.crashes_found == 'true'
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-crashes-${{ matrix.target }}-${{ github.sha }}
          path: fuzz/artifacts/${{ matrix.target }}/
          retention-days: 90

      - name: Create issue for crashes
        if: steps.check_crashes.outputs.crashes_found == 'true' && github.event_name == 'schedule'
        uses: actions/github-script@v8
        with:
          script: |
            const fs = require('fs');
            const target = '${{ matrix.target }}';
            const artifactsDir = `fuzz/artifacts/${target}`;
            const files = fs.readdirSync(artifactsDir);

            const body = `
            ## 🐛 Fuzzing Found Crashes in \`${target}\`

            The continuous fuzzing workflow found ${files.length} crash(es) in the \`${target}\` fuzz target.

            ### Crash Files
            ${files.map(f => `- \`${f}\``).join('\n')}

            ### How to Reproduce
            \`\`\`bash
            cd fuzz
            cargo fuzz run ${target} fuzz/artifacts/${target}/${files[0]}
            \`\`\`

            ### Artifacts
            Download crash artifacts from: [Actions Run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})

            ### Next Steps
            1. Reproduce locally using the command above
            2. Fix the underlying issue
            3. Add a regression test
            4. Re-run fuzzing to verify the fix

            ---
            *Automatically created by continuous fuzzing workflow*
            `;

            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `[FUZZ] Crashes found in ${target}`,
              body: body,
              labels: ['bug', 'fuzzing', 'security']
            });

      - name: Fail if crashes found
        if: steps.check_crashes.outputs.crashes_found == 'true'
        run: |
          echo "::error::Fuzzing found crashes in ${{ matrix.target }}"
          exit 1

  # Corpus statistics and coverage
  fuzz-coverage:
    name: Fuzz Coverage Report
    runs-on: ubuntu-latest
    needs: fuzz-continuous
    if: always() && github.event_name == 'schedule'
    steps:
      - uses: actions/checkout@v6

      - uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: nightly

      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: "fuzz -> target"

      - name: Install cargo-fuzz
        run: cargo install cargo-fuzz

      - name: Restore all corpora
        uses: actions/cache@v5
        with:
          path: fuzz/corpus/
          key: fuzz-corpus-all-${{ github.sha }}
          restore-keys: |
            fuzz-corpus-

      - name: Generate coverage report
        run: |
          cd fuzz
          # Sample arbitrary_ops targets for coverage (representative subset)
          # This avoids running coverage for all targets which would be too slow
          TARGETS=$(cargo fuzz list | grep '_arbitrary_ops$' || true)

          if [ -z "$TARGETS" ]; then
            echo "No arbitrary_ops targets found, using all targets" >> $GITHUB_STEP_SUMMARY
            TARGETS=$(cargo fuzz list)
          fi

          echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          for target in $TARGETS; do
            echo "### Coverage for $target" >> $GITHUB_STEP_SUMMARY
            cargo fuzz coverage $target || true
            if [ -f "coverage/$target/coverage.json" ]; then
              echo "\`\`\`json" >> $GITHUB_STEP_SUMMARY
              cat "coverage/$target/coverage.json" >> $GITHUB_STEP_SUMMARY
              echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
            fi
            echo "" >> $GITHUB_STEP_SUMMARY
          done

      - name: Upload coverage artifacts
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-coverage-${{ github.sha }}
          path: fuzz/coverage/
          retention-days: 30

  # Summary job
  fuzz-summary:
    name: Fuzzing Summary
    runs-on: ubuntu-latest
    needs: fuzz-continuous
    if: always()
    steps:
      - name: Check fuzzing results
        run: |
          if [ "${{ needs.fuzz-continuous.result }}" != "success" ]; then
            echo "⚠️ Some fuzz targets found issues or failed" >> $GITHUB_STEP_SUMMARY
            exit 1
          else
            echo "✅ All fuzz targets passed" >> $GITHUB_STEP_SUMMARY
          fi
