name: Validate Framework Integrity

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

permissions:
  contents: read

env:
  # Force UTF-8 for all Python I/O across the matrix. Windows runners default to
  # cp1252, which has previously broken this repo's cross-platform tooling. (#163)
  PYTHONUTF8: "1"

concurrency:
  # Group by branch (head_ref on PRs, ref_name on push) so a new push cancels the
  # prior in-flight run for the same branch rather than across all refs. (#163)
  group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
  cancel-in-progress: true

jobs:
  # Detect whether the change touches anything beyond pure documentation. Heavy
  # jobs (Windows pytest, deploy smoke tests, ubuntu pytest) are skipped on
  # docs-only changes — they can't regress when no code/governance/CI moved. The
  # three branch-protection-required checks (Framework Validation, ShellCheck,
  # Check Markdown Links) are intentionally NOT gated, so protection still holds.
  changes:
    name: Detect change scope
    runs-on: ubuntu-latest
    outputs:
      heavy: ${{ steps.scope.outputs.heavy }}
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Classify changed files (docs-only -> skip heavy jobs)
        id: scope
        shell: bash
        run: |
          set -uo pipefail
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            base_sha="${{ github.event.pull_request.base.sha }}"
            head_sha="${{ github.event.pull_request.head.sha }}"
          else
            base_sha="${{ github.event.before }}"
            head_sha="${{ github.sha }}"
          fi
          zero="0000000000000000000000000000000000000000"
          if [ -z "$base_sha" ] || [ "$base_sha" = "$zero" ]; then
            echo "No usable base sha ($base_sha) — running full suite (fail-safe)."
            echo "heavy=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          if ! files="$(git diff --name-only "${base_sha}...${head_sha}" 2>/dev/null)"; then
            echo "git diff failed — running full suite (fail-safe)."
            echo "heavy=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          echo "Changed files:"
          printf '%s\n' "$files"
          heavy=false
          while IFS= read -r f; do
            [ -z "$f" ] && continue
            case "$f" in
              # Inert documentation surfaces — never exercised by the test suite.
              README.md|README_*.md|llms.txt|LICENSE|CHANGELOG.md|CONTRIBUTING.md|CITATION.cff) ;;
              docs/*|.agentcortex/context/*|.agentcortex/docs/*) ;;
              # Anything else (code, AGENTS.md/.agent governance, tests, CI) is heavy.
              *) heavy=true; break ;;
            esac
          done <<< "$files"
          echo "Heavy (code/test/deploy) jobs needed: $heavy"
          echo "heavy=$heavy" >> "$GITHUB_OUTPUT"

  validate:
    name: Framework Validation
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      - name: Run validate.sh
        run: |
          chmod +x .agentcortex/bin/validate.sh
          .agentcortex/bin/validate.sh

  validate-python39:
    name: Framework Validation (Python 3.9)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.9'

      - name: Run validate.sh
        run: |
          chmod +x .agentcortex/bin/validate.sh
          .agentcortex/bin/validate.sh

  shellcheck:
    name: ShellCheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Lint shell scripts
        run: |
          shopt -s globstar nullglob
          scripts=()
          for f in **/*.sh; do
            [[ -f "$f" ]] || continue
            scripts+=("$f")
          done
          for f in .githooks/*.sample; do      # opt-in hooks are bash w/ shebang (backlog #74)
            [[ -f "$f" ]] || continue
            scripts+=("$f")
          done
          if [[ ${#scripts[@]} -eq 0 ]]; then
            echo "No shell scripts found"
            exit 0
          fi
          failed=0
          for f in "${scripts[@]}"; do
            echo "::group::$f"
            if ! shellcheck -S warning -x "$f"; then
              failed=1
            fi
            echo "::endgroup::"
          done
          exit $failed

  markdown-links:
    name: Check Markdown Links
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Check internal links
        run: |
          status=0
          while IFS= read -r file; do
            while IFS= read -r link; do
              # Skip external URLs, anchors, and mailto
              case "$link" in
                http://*|https://*|mailto:*|\#*) continue ;;
              esac
              # Strip anchor from path
              path="${link%%#*}"
              [ -z "$path" ] && continue
              # Resolve relative to file's directory
              dir="$(dirname "$file")"
              target="$dir/$path"
              if [ ! -e "$target" ]; then
                echo "::error file=$file::Broken link: $link"
                status=1
              fi
            done < <(grep -oP '\[.*?\]\(\K[^)]+' "$file" 2>/dev/null)
          done < <(find . -name '*.md' -not -path './.git/*')
          exit $status

  validate-windows:
    name: Framework Validation (Windows)
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      - name: Run validate.ps1
        shell: pwsh
        run: |
          .agentcortex/bin/validate.ps1

  deploy-smoke-test:
    name: Deploy Smoke Test
    runs-on: ubuntu-latest
    needs: changes
    if: needs.changes.outputs.heavy == 'true'
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      - name: Create test project and deploy
        run: |
          mkdir -p /tmp/test-project
          cd /tmp/test-project
          git init
          # Use canonical deploy.sh directly (deploy_brain.sh is a wrapper
          # meant for downstream repos, not source-repo CI).
          bash "$GITHUB_WORKSPACE/.agentcortex/bin/deploy.sh" .

      - name: Validate downstream project
        run: |
          cd /tmp/test-project
          bash .agentcortex/bin/validate.sh

  test-ci-structural:
    name: CI Structural Tests
    runs-on: ubuntu-latest
    needs: changes
    if: needs.changes.outputs.heavy == 'true'
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-python@v6
        with:
          python-version: '3.12'
          cache: 'pip'
          cache-dependency-path: .github/requirements-ci.txt

      - name: Install test dependencies
        run: pip install -r .github/requirements-ci.txt

      - name: Run CI structural + guard + framework tests
        run: python -m pytest tests/ci/ tests/guard/ .agentcortex/tests/ -v

  test-windows:
    name: Pytest (Windows)
    runs-on: windows-latest
    needs: changes
    if: needs.changes.outputs.heavy == 'true'
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-python@v6
        with:
          python-version: '3.12'
          cache: 'pip'
          cache-dependency-path: .github/requirements-ci.txt

      - name: Install test dependencies
        run: pip install -r .github/requirements-ci.txt

      # Windows previously ran only validate.ps1 — cross-platform pytest
      # regressions (CRLF, path separators, cp1252) went uncaught. (#163)
      - name: Run full test suite on Windows
        run: python -m pytest tests/ci/ tests/guard/ .agentcortex/tests/ -q

  utf8-and-critical-files:
    name: UTF-8 Sweep + Critical Files
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      # File-validity sweep: every tracked .md/.sh/.yml/.yaml must decode as
      # UTF-8 (cp1252/latin-1 files have repeatedly broken cross-platform
      # validator tooling), plus a presence pre-check for governance-critical
      # files before anything else relies on them. (#163)
      - name: UTF-8 file-validity sweep + critical-file presence
        run: |
          python - <<'PYEOF'
          import subprocess, sys
          from pathlib import Path

          critical = [
              "AGENTS.md", "CLAUDE.md", "GEMINI.md",
              ".agent/config.yaml",
              ".agentcortex/bin/validate.sh", ".agentcortex/bin/validate.ps1",
              ".agentcortex/bin/deploy.sh",
          ]
          missing = [c for c in critical if not Path(c).is_file()]
          if missing:
              print(f"::error::critical file(s) missing: {missing}")
              sys.exit(1)

          tracked = subprocess.run(
              ["git", "ls-files", "*.md", "*.sh", "*.yml", "*.yaml"],
              capture_output=True, text=True, check=True,
          ).stdout.splitlines()
          bad = []
          for name in tracked:
              try:
                  Path(name).read_bytes().decode("utf-8")
              except (UnicodeDecodeError, OSError) as exc:
                  bad.append((name, str(exc)))
          for name, err in bad:
              print(f"::error file={name}::not valid UTF-8: {err}")
          print(f"checked {len(tracked)} tracked files; critical files present: {len(critical)}")
          sys.exit(1 if bad else 0)
          PYEOF

  deploy-no-python:
    name: Deploy Smoke Test (No Python)
    runs-on: ubuntu-latest
    needs: changes
    if: needs.changes.outputs.heavy == 'true'
    steps:
      - uses: actions/checkout@v6

      - name: Create test project and deploy without Python
        run: |
          mkdir -p /tmp/test-no-python
          cd /tmp/test-no-python
          git init
          bash "$GITHUB_WORKSPACE/.agentcortex/bin/deploy.sh" .

      - name: Validate downstream project without Python
        run: |
          cd /tmp/test-no-python
          bash .agentcortex/bin/validate.sh --no-python
