name: Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    # Run nightly tests every Sunday at 2 AM UTC (9 PM Saturday PST)
    - cron: '0 2 * * 0'
  workflow_dispatch:  # Allow manual trigger any time

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

jobs:
  performance-evidence:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Validate Performance Evidence section
        run: python scripts/check_performance_evidence.py --github-event "$GITHUB_EVENT_PATH"

  # Detect whether code paths changed — skip tests for docs-only PRs
  detect-changes:
    if: github.event_name == 'pull_request' || github.event_name == 'push'
    runs-on: ubuntu-latest
    outputs:
      code: ${{ steps.filter.outputs.code }}
    steps:
      - uses: actions/checkout@v6
        with:
          # For PRs the base SHA is fetched automatically; for pushes we only
          # need the previous commit.  Fall back to full clone only when the
          # shallow history doesn't contain the base SHA.
          fetch-depth: 20

      - name: Check for code changes
        id: filter
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE=${{ github.event.pull_request.base.sha }}
          else
            BASE=${{ github.event.before }}
          fi
          # List changed files between base and head
          CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "FORCE_RUN")

          # If git diff failed (e.g. force push with unknown base), run everything
          if echo "$CHANGED" | grep -q "FORCE_RUN"; then
            echo "code=true" >> $GITHUB_OUTPUT
            echo "Could not determine base — running all jobs"
            exit 0
          fi

          # Check if any changed file is outside docs-only paths
          CODE_CHANGED="false"
          while IFS= read -r file; do
            case "$file" in
              site/*|docs/*|plan/*|skills/*|*.md|LICENSE|.context/*) ;;
              *) CODE_CHANGED="true"; break ;;
            esac
          done <<< "$CHANGED"

          echo "code=$CODE_CHANGED" >> $GITHUB_OUTPUT
          if [ "$CODE_CHANGED" = "false" ]; then
            echo "Docs-only change — skipping test jobs"
          else
            echo "Code changed — running test jobs"
          fi

  # Fast check: unit tests only for quick PR feedback
  fast-check:
    needs: detect-changes
    if: needs.detect-changes.outputs.code == 'true'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Fast test suite (unit tests + coverage)
      timeout-minutes: 25
      env:
        CI: true
        BENGAL_CI_FAST: "1"
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        echo "Running fast test suite (unit tests only, excluding slow)..."
        uv run pytest -n 4 -q --tb=short --maxfail=5 --dist worksteal --max-worker-restart=3 --cov=bengal --cov-report=xml tests/unit tests/core tests/test_thread_safety.py -m "not slow and not known_gap"

    - name: Upload coverage
      uses: codecov/codecov-action@v7
      with:
        files: coverage.xml

  # Baseurl spot-checks (parallel with downstream test jobs)
  baseurl:
    needs: fast-check
    if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        baseurl: ["/bengal", "https://example.com/sub"]

    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Run targeted baseurl portability tests
      timeout-minutes: 10
      env:
        BENGAL_BASEURL: ${{ matrix.baseurl }}
        CI: true
        BENGAL_CI_FAST: "1"
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        echo "Running with BENGAL_BASEURL='${BENGAL_BASEURL}'"
        uv run pytest -q --tb=short --maxfail=5 -n auto tests/unit/config/test_config_loader_baseurl_env.py tests/unit/rendering/test_asset_url_baseurl.py tests/unit/rendering/test_template_links_baseurl.py tests/integration/test_baseurl_builds.py

  # E2E tests (build + validate + check links)
  e2e:
    needs: fast-check
    if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Run E2E tests
      env:
        CI: true
        BENGAL_CI_FAST: "1"
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        echo "Running E2E tests..."
        uv run pytest \
          tests/integration/test_cli_smoke.py \
          tests/integration/test_documentation_builds.py \
          tests/integration/test_blog_scenarios.py \
          tests/integration/test_golden_output.py \
          tests/integration/test_full_site_url_consistency.py \
          -v --tb=short -n auto --dist worksteal --max-worker-restart=3

  # Integration tests (excluding warm_build/ — see warm-build job)
  integration:
    needs: fast-check
    if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2, 3, 4, 5, 6]
    # 15m was too tight for slowest shards (runner cancels mid-run). 25m + 6 shards
    # keeps wall time per shard well under the limit.
    timeout-minutes: 25
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Integration tests (shard ${{ matrix.group }}/6, excludes warm_build/)
      env:
        CI: true
        BENGAL_CI_FAST: "1"
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
        PYTHONUNBUFFERED: "1"
      run: |
        echo "Running integration tests (group ${{ matrix.group }}/6)..."
        set -o pipefail
        uv run pytest \
          --splits 6 --group ${{ matrix.group }} \
          --splitting-algorithm least_duration \
          -n 1 -vv -ra --tb=short --maxfail=3 --durations=40 \
          --timeout=180 --timeout-method=thread \
          --max-worker-restart=3 \
          tests/integration \
          --ignore=tests/integration/warm_build \
          -m "not slow and not performance and not stateful and not autodoc and not known_gap" \
          --junitxml=integration-results.xml \
          2>&1 | tee integration-tests.log

    - name: Upload integration diagnostics
      if: always()
      uses: actions/upload-artifact@v7
      with:
        name: integration-diagnostics-group${{ matrix.group }}
        path: |
          integration-tests.log
          integration-results.xml
        if-no-files-found: warn

  # Warm-build integration tests (isolated; heavy full/incremental site builds)
  warm-build:
    needs: fast-check
    if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
    timeout-minutes: 20
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Warm build tests (shard ${{ matrix.group }}/2)
      env:
        CI: true
        BENGAL_CI_FAST: "1"
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
        PYTHONUNBUFFERED: "1"
      run: |
        echo "Running warm_build integration tests (group ${{ matrix.group }}/2)..."
        set -o pipefail
        uv run pytest \
          --splits 2 --group ${{ matrix.group }} \
          --splitting-algorithm least_duration \
          -n auto -vv -ra --tb=short --maxfail=1 --durations=20 \
          --timeout=60 --timeout-method=thread \
          --dist worksteal --max-worker-restart=3 \
          tests/integration/warm_build/ \
          -m "not slow and not performance and not stateful" \
          --junitxml=warm-build-results.xml \
          2>&1 | tee warm-build-tests.log

    - name: Upload warm-build diagnostics
      if: always()
      uses: actions/upload-artifact@v7
      with:
        name: warm-build-diagnostics-group${{ matrix.group }}
        path: |
          warm-build-tests.log
          warm-build-results.xml
        if-no-files-found: warn

  # Render output regression sentinels (autodoc + fallback path)
  render-output-regression:
    needs: fast-check
    if: ${{ !cancelled() && needs.fast-check.result == 'success' }}
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Run render output regression tests
      timeout-minutes: 10
      env:
        CI: true
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        uv run pytest -n 0 -q --tb=short -m performance \
          tests/performance/test_autodoc_render_regression.py \
          tests/performance/test_asset_fallback_cost.py

    - name: Upload render output metrics
      if: always()
      uses: actions/upload-artifact@v7
      with:
        name: render-output-metrics-${{ github.sha }}
        path: |
          .pytest_cache/render_output_metrics.json
          .pytest_cache/fallback_parser_metrics.json
        if-no-files-found: warn

  # Slow tests (memory profiling, bytecode cache, etc.)
  slow-tests:
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Run slow tests
      timeout-minutes: 30
      env:
        CI: true
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        uv run pytest -n auto -q --tb=short -m "(slow or known_gap) and not stateful and not performance" --dist loadgroup --max-worker-restart=3

  # Stateful workflows (weekly/manual)
  stateful:
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Run stateful tests (xdist hardened)
      timeout-minutes: 30
      env:
        CI: true
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        uv run pytest -n auto -q --tb=short -m stateful --dist worksteal --max-worker-restart=2 -p pytest_forked --forked

  # Performance suite (weekly/manual)
  performance:
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6

    - name: Install uv
      uses: astral-sh/setup-uv@v7
      with:
        python-version: "3.14t"

    - name: Cache uv dependencies
      uses: actions/cache@v5
      with:
        path: ~/.cache/uv
        key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
        restore-keys: |
          uv-${{ runner.os }}-

    - name: Install dependencies
      run: uv sync --no-sources --group dev

    - name: Run performance tests
      timeout-minutes: 30
      env:
        CI: true
        OMP_NUM_THREADS: "1"
        PYTHON_GIL: "0"
      run: |
        uv run pytest -n auto -q --tb=short -m performance --dist worksteal --max-worker-restart=2

  # Gate job for branch protection — succeeds when tests pass OR were skipped (docs-only)
  ci-ok:
    if: always()
    needs:
      - detect-changes
      - fast-check
      - baseurl
      - e2e
      - integration
      - warm-build
      - render-output-regression
    runs-on: ubuntu-latest
    steps:
      - name: Decide status
        run: |
          # If detect-changes decided no code changed, all test jobs are skipped — that's OK
          if [ "${{ needs.detect-changes.outputs.code }}" = "false" ]; then
            echo "Docs-only change — all tests skipped, CI passes"
            exit 0
          fi
          # Otherwise, check that no test job failed
          if [ "${{ needs.fast-check.result }}" = "failure" ] || \
             [ "${{ needs.baseurl.result }}" = "failure" ] || \
             [ "${{ needs.e2e.result }}" = "failure" ] || \
             [ "${{ needs.integration.result }}" = "failure" ] || \
             [ "${{ needs.warm-build.result }}" = "failure" ] || \
             [ "${{ needs.render-output-regression.result }}" = "failure" ]; then
            echo "One or more test jobs failed"
            exit 1
          fi
          echo "All test jobs passed"
