# Primary CI workflow.
#
# Only runs against packages that have changed files.
#
# Runs:
# - Linting (_lint.yml)
# - Unit Tests (_test.yml)
# - Pydantic compatibility tests (_test_pydantic.yml)
# - Integration test compilation checks (_compile_integration_test.yml)
# - Extended test suites that require additional dependencies
#
# Reports status to GitHub checks and PR status.

name: "🔧 CI"

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

# Optimizes CI performance by canceling redundant workflow runs
# If another push to the same PR or branch happens while this workflow is still running,
# cancel the earlier run in favor of the next run.
#
# There's no point in testing an outdated version of the code. GitHub only allows
# a limited number of job runners to be active at the same time, so it's better to
# cancel pointless jobs early so that more useful jobs can run sooner.
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  UV_FROZEN: "true"
  UV_NO_SYNC: "true"

jobs:
  # This job analyzes which files changed and creates a dynamic test matrix
  # to only run tests/lints for the affected packages, improving CI efficiency
  build:
    name: "Detect Changes & Set Matrix"
    runs-on: ubuntu-latest
    if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci-ignore') }}
    steps:
      - name: "📋 Checkout Code"
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: "🐍 Setup Python 3.11"
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.11"
      - name: "📂 Get Changed Files"
        id: files
        uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c # v2.3.0
        with:
          format: json
      - name: "🔍 Analyze Changed Files & Generate Build Matrix"
        id: set-matrix
        env:
          ALL_CHANGED_FILES: ${{ steps.files.outputs.all }}
        run: |
          python -m pip install packaging requests
          python .github/scripts/check_diff.py "$ALL_CHANGED_FILES" >> $GITHUB_OUTPUT
    outputs:
      lint: ${{ steps.set-matrix.outputs.lint }}
      test: ${{ steps.set-matrix.outputs.test }}
      extended-tests: ${{ steps.set-matrix.outputs.extended-tests }}
      compile-integration-tests: ${{ steps.set-matrix.outputs.compile-integration-tests }}
      dependencies: ${{ steps.set-matrix.outputs.dependencies }}
      test-pydantic: ${{ steps.set-matrix.outputs.test-pydantic }}
      vcr-tests: ${{ steps.set-matrix.outputs.vcr-tests }}
  # Run linting only on packages that have changed files
  lint:
    needs: [build]
    if: ${{ needs.build.outputs.lint != '[]' }}
    strategy:
      matrix:
        job-configs: ${{ fromJson(needs.build.outputs.lint) }}
      fail-fast: false
    uses: ./.github/workflows/_lint.yml
    with:
      working-directory: ${{ matrix.job-configs.working-directory }}
      python-version: ${{ matrix.job-configs.python-version }}
    secrets: inherit

  # Run unit tests only on packages that have changed files
  test:
    needs: [build]
    if: ${{ needs.build.outputs.test != '[]' }}
    strategy:
      matrix:
        job-configs: ${{ fromJson(needs.build.outputs.test) }}
      fail-fast: false
    uses: ./.github/workflows/_test.yml
    with:
      working-directory: ${{ matrix.job-configs.working-directory }}
      python-version: ${{ matrix.job-configs.python-version }}
    secrets: inherit

  # Test compatibility with different Pydantic versions for affected packages
  test-pydantic:
    needs: [build]
    if: ${{ needs.build.outputs.test-pydantic != '[]' }}
    strategy:
      matrix:
        job-configs: ${{ fromJson(needs.build.outputs.test-pydantic) }}
      fail-fast: false
    uses: ./.github/workflows/_test_pydantic.yml
    with:
      working-directory: ${{ matrix.job-configs.working-directory }}
      pydantic-version: ${{ matrix.job-configs.pydantic-version }}
    secrets: inherit

  # Verify integration tests compile without actually running them (faster feedback)
  compile-integration-tests:
    name: "Compile Integration Tests"
    needs: [build]
    if: ${{ needs.build.outputs.compile-integration-tests != '[]' }}
    strategy:
      matrix:
        job-configs: ${{ fromJson(needs.build.outputs.compile-integration-tests) }}
      fail-fast: false
    uses: ./.github/workflows/_compile_integration_test.yml
    with:
      working-directory: ${{ matrix.job-configs.working-directory }}
      python-version: ${{ matrix.job-configs.python-version }}
    secrets: inherit

  # Run VCR cassette-backed integration tests in playback-only mode (no API keys)
  vcr-tests:
    name: "VCR Cassette Tests"
    needs: [build]
    if: ${{ needs.build.outputs.vcr-tests != '[]' }}
    strategy:
      matrix:
        job-configs: ${{ fromJson(needs.build.outputs.vcr-tests) }}
      fail-fast: false
    uses: ./.github/workflows/_test_vcr.yml
    with:
      working-directory: ${{ matrix.job-configs.working-directory }}
      python-version: ${{ matrix.job-configs.python-version }}
    secrets: inherit

  # Run extended test suites that require additional dependencies
  extended-tests:
    name: "Extended Tests"
    needs: [build]
    if: ${{ needs.build.outputs.extended-tests != '[]' }}
    strategy:
      matrix:
        # note different variable for extended test dirs
        job-configs: ${{ fromJson(needs.build.outputs.extended-tests) }}
      fail-fast: false
    runs-on: ubuntu-latest
    timeout-minutes: 20
    defaults:
      run:
        working-directory: ${{ matrix.job-configs.working-directory }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: "🐍 Set up Python ${{ matrix.job-configs.python-version }} + UV"
        uses: "./.github/actions/uv_setup"
        with:
          python-version: ${{ matrix.job-configs.python-version }}
          cache-suffix: extended-tests-${{ matrix.job-configs.working-directory }}
          working-directory: ${{ matrix.job-configs.working-directory }}

      - name: "📦 Install Dependencies & Run Extended Tests"
        shell: bash
        run: |
          echo "Running extended tests, installing dependencies with uv..."
          uv venv
          uv sync --group test
          VIRTUAL_ENV=.venv uv pip install -r extended_testing_deps.txt
          VIRTUAL_ENV=.venv make extended_tests

      - name: "🧹 Verify Clean Working Directory"
        shell: bash
        run: |
          set -eu

          STATUS="$(git status)"
          echo "$STATUS"

          # grep will exit non-zero if the target message isn't found,
          # and `set -e` above will cause the step to fail.
          echo "$STATUS" | grep 'nothing to commit, working tree clean'

  # Verify _release.yml dropdown options stay in sync with package directories
  check-release-options:
    name: "Validate Release Options"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: "🐍 Setup Python 3.11"
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.11"
      - name: "📦 Install Dependencies"
        run: python -m pip install pyyaml pytest
      - name: "🔍 Check release dropdown matches packages"
        run: python -m pytest .github/scripts/test_release_options.py -v

  # Final status check - ensures all required jobs passed before allowing merge
  ci_success:
    name: "✅ CI Success"
    needs:
      [
        build,
        lint,
        test,
        compile-integration-tests,
        vcr-tests,
        extended-tests,
        test-pydantic,
        check-release-options,
      ]
    if: |
      always()
    runs-on: ubuntu-latest
    env:
      JOBS_JSON: ${{ toJSON(needs) }}
      RESULTS_JSON: ${{ toJSON(needs.*.result) }}
      EXIT_CODE: ${{!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && '0' || '1'}}
    steps:
      - name: "🎉 All Checks Passed"
        run: |
          echo $JOBS_JSON
          echo $RESULTS_JSON
          echo "Exiting with $EXIT_CODE"
          exit $EXIT_CODE
