name: CI

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

permissions:
  contents: write
  id-token: write

# Cancel in-progress runs for massive time savings
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: 0                              # Better caching
  CARGO_NET_RETRY: 10                               # Reliability
  CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse       # 3x faster registry fetching
  CARGO_NET_GIT_FETCH_WITH_CLI: "true"              # Use git CLI for fetching
  RUST_BACKTRACE: 1
  RUSTFLAGS: "-C link-arg=-fuse-ld=lld -D warnings" # LLD linker for 2-3x faster linking
  NIX_CONFIG: "experimental-features = nix-command flakes"

jobs:
  # COMMIT LINT: Fast check, no build dependency
  commit-lint:
    name: Commit Lint
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Check commit messages
        run: |
          git log --format=%B origin/${{ github.base_ref }}..HEAD | while read -r msg; do
            [ -z "$msg" ] && continue
            if echo "$msg" | grep -qiE "^(wip|fixup|squash)"; then
              echo "❌ ERROR: WIP commit found: $msg"
              exit 1
            fi
          done
          echo "✅ Commit messages look good"

  detect-migration-script-changes:
    name: Detect Migration Script Changes
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    outputs:
      changed: ${{ steps.diff.outputs.changed }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Detect changed migration scripts
        id: diff
        run: |
          set -euo pipefail
          base_ref="${{ github.base_ref }}"
          git fetch --no-tags --prune origin "$base_ref"
          changed_files="$(git diff --name-only "origin/$base_ref"...HEAD)"
          echo "$changed_files"
          if echo "$changed_files" | grep -qE '^src/core/sql/.*\.sql$'; then
            echo "changed=true" >> "$GITHUB_OUTPUT"
          else
            echo "changed=false" >> "$GITHUB_OUTPUT"
          fi

  # FMT: Pure source check, no compile needed
  fmt:
    name: Lint (fmt)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
      - name: Install Nix
        uses: DeterminateSystems/nix-installer-action@main
      - name: Enable Nix cache
        uses: DeterminateSystems/magic-nix-cache-action@main
      - name: Format check
        run: nix develop .#ci -c cargo fmt --all -- --check

  # CLIPPY: Compiles its own analysis pass, runs in parallel with setup
  clippy:
    name: Lint (clippy)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install Nix
        uses: DeterminateSystems/nix-installer-action@main
      - name: Enable Nix cache
        uses: DeterminateSystems/magic-nix-cache-action@main
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Clippy
        run: nix develop .#ci -c cargo clippy --all-targets --all-features -- -D warnings

  # HEALTH: consumes the shared decapod binary from setup
  health:
    name: Health Checks
    runs-on: ubuntu-latest
    needs: setup
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Download shared decapod binary
        uses: actions/download-artifact@v8
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug
      - name: Install shared decapod binary
        run: |
          chmod +x target/debug/decapod
          echo "$PWD/target/debug" >> "$GITHUB_PATH"
      - name: Initialize Decapod
        run: decapod init --force
        env:
          DECAPOD_VALIDATE_SKIP_GIT_GATES: "1"
      - name: Acquire Decapod session
        run: decapod session acquire
        env:
          DECAPOD_VALIDATE_SKIP_GIT_GATES: "1"
      - name: Run validation harness
        run: decapod validate
        env:
          DECAPOD_VALIDATE_SKIP_GIT_GATES: "1"
          DECAPOD_VALIDATE_TIMEOUT_SECS: "120"
      - name: Run watcher
        run: decapod govern watcher run
        env:
          DECAPOD_VALIDATE_SKIP_GIT_GATES: "1"
      - name: Check health summary
        env:
          DECAPOD_VALIDATE_SKIP_GIT_GATES: "1"
        run: |
          STATUS=$(decapod govern health summary)
          echo "$STATUS"
          if echo "$STATUS" | grep -q '"watcher_stale": true'; then
            echo "::warning::Watcher is stale"
          fi

  # STATE_COMMIT: Builds its own golden_vectors binary, no dependency on setup
  state-commit-golden-vectors:
    name: Golden Vectors (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    env:
      DECAPOD_STATE_COMMIT_CI_JOB: state-commit-golden-vectors
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          submodules: recursive
          persist-credentials: false
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Verify golden vectors exist
        run: |
          echo "=== Verifying golden vectors exist ==="
          test -f tests/golden/state_commit/v1/scope_record.cbor || exit 1
          test -f tests/golden/state_commit/v1/scope_record_hash.txt || exit 1
          test -f tests/golden/state_commit/v1/state_commit_root.txt || exit 1
          echo "=== Expected values ==="
          echo "scope_record_hash: 41d7e3729b6f4512887fb3cb6f10140942b600041e0d88308b0177e06ebb4b93"
          echo "state_commit_root: 28591ac86e52ffac76d5fc3aceeceda5d8592708a8d7fcb75371567fdc481492"
          echo "=== Actual committed values ==="
          cat tests/golden/state_commit/v1/scope_record_hash.txt
          cat tests/golden/state_commit/v1/state_commit_root.txt
      - name: Build golden vectors generator
        run: cd tests/golden_vectors && unset RUSTFLAGS && cargo build --release
      - name: Initialize fixture repo
        run: |
          cd tests/fixtures/state_commit_repo
          git init
          git config user.email "test@test.com"
          git config user.name "Test"
          git add initial.txt
          git commit -m "base commit"
          git add .
          git commit -m "fixture commit"
      - name: Run golden vectors generator
        working-directory: tests/golden_vectors
        run: |
          ./target/release/golden_vectors > /tmp/generated.txt 2>&1
          GEN_HASH=$(grep "scope_record_hash:" /tmp/generated.txt | awk '{print $2}')
          GEN_ROOT=$(grep "state_commit_root:" /tmp/generated.txt | awk '{print $2}')
          echo "Generated hash: $GEN_HASH"
          echo "Generated root: $GEN_ROOT"
          test -n "$GEN_HASH" || exit 1
          test -n "$GEN_ROOT" || exit 1
          echo "✅ Golden vectors generator runs successfully"
      - name: Verify no network dependencies
        run: |
          if grep -qE "(reqwest|hyper|ureq|http)" tests/golden_vectors/Cargo.toml; then
            echo "ERROR: golden_vectors has network dependencies"
            exit 1
          fi
          echo "✅ No network dependencies"

  # SETUP: Compile the shared decapod binary once and save both cache + binary artifact.
  # Test jobs restore this cache and download the same target/debug/decapod executable
  # so integration tests that use CARGO_BIN_EXE_decapod do not independently rebuild it.
  setup:
    name: Setup & Build Shared Binary
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - name: Save Rust cache (always, so PRs benefit on next run)
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          cache-targets: true
          save-if: "true"
          cache-all-crates: true
      - name: Check Cargo.lock is up to date
        run: cargo generate-lockfile && git diff --quiet Cargo.lock || echo "⚠️ Cargo.lock outdated - run cargo update locally"
      - name: Compile shared decapod binary
        run: cargo build --bin decapod
      - name: Upload shared decapod binary
        uses: actions/upload-artifact@v7
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug/decapod
          retention-days: 1
          compression-level: 0

# SELECTIVE TESTS: Run only tests for changed files (PRs) or all tests (master)
  test:
    name: Tests (${{ matrix.target }})
    runs-on: ubuntu-latest
    needs: setup
    if: github.event_name == 'pull_request'
    strategy:
      fail-fast: false
      matrix:
        target: [core, plugins, cli, integration]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Detect changed files
        id: changed
        run: |
          base_ref="${{ github.base_ref }}"
          git fetch --no-tags --prune origin "$base_ref"
          chg=$(git diff --name-only "origin/$base_ref"...HEAD | tr '\n' ',' | sed 's/,$//')
          echo "changed=$chg" >> "$GITHUB_OUTPUT"
      - name: Run selective tests
        run: |
          set -euo pipefail
          CHANGED="${{ steps.changed.outputs.changed }}"
          echo "Changed files: $CHANGED"
          
          # decapod impact analysis
          decapod impact --changed-files "$CHANGED" --predict --format text || true
          
          case "${{ matrix.target }}" in
            core)
              if echo "$CHANGED" | grep -qE 'src/core/(todo|validate|workspace|workunit|obligation|docs|capsule|rpc|migration|gateway)'; then
                cargo test --test todo_enforcement -- --test-threads=4
                cargo test --test validate_termination -- --test-threads=4
                cargo test --test workspace_interlock -- --test-threads=4
                cargo test --test workunit_cli -- --test-threads=4
                cargo test --test context_capsule_cli -- --test-threads=4
              elif echo "$CHANGED" | grep -qE 'src/core/'; then
                cargo test --test entrypoint_correctness -- --test-threads=4
              fi
              ;;
            plugins)
              if echo "$CHANGED" | grep -qE 'src/plugins/'; then
                cargo test --test plugins_todo_tests -- --test-threads=4
                cargo test --test plugins_policy_tests -- --test-threads=4
                cargo test --test plugins_health_tests -- --test-threads=4
                cargo test --test plugins_aptitude_tests -- --test-threads=4
                cargo test --test plugins_internalize_tests -- --test-threads=4
                cargo test --test plugins_federation_tests -- --test-threads=4
                cargo test --test plugins_decide_tests -- --test-threads=4
                cargo test --test plugins_obligation_tests -- --test-threads=4
              fi
              ;;
            cli)
              if echo "$CHANGED" | grep -qE 'src/(cli|lib|main)'; then
                cargo test --test cli_contract_enforcement -- --test-threads=4
                cargo test --test entrypoint_correctness -- --test-threads=4
                cargo test --test cli_contracts -- --test-threads=4
              fi
              ;;
            integration)
              if echo "$CHANGED" | grep -qE 'src/'; then
                cargo test --test contract_conformance -- --test-threads=4
                cargo test --test constitution_claims -- --test-threads=4
                cargo test --test spec_conformance -- --test-threads=4
                cargo test --test substrate_integrity -- --test-threads=4
              fi
              ;;
          esac
          echo "✓ Selective tests passed"

  # MASTER: Full test suite for direct master pushes only
  test-master:
    name: Tests (Full)
    runs-on: ubuntu-latest
    needs: setup
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Run all tests
        run: cargo test --test gatling -- --test-threads=4

  # RPC SUITE: isolated to avoid contention
  test-rpc-suite:
    name: Tests (RPC Suite)
    runs-on: ubuntu-latest
    needs: setup
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install Nix
        uses: DeterminateSystems/nix-installer-action@main
      - name: Enable Nix cache
        uses: DeterminateSystems/magic-nix-cache-action@main
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Download shared decapod binary
        uses: actions/download-artifact@v8
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug
      - name: Make shared decapod binary executable
        run: chmod +x target/debug/decapod
      - name: Run RPC suite
        run: nix develop .#ci -c cargo test --test agent_rpc_suite -- --test-threads=1

  # CHAOS: Multi-agent contention + deterministic replay gate
  chaos-replay:
    name: Chaos Replay Gate
    runs-on: ubuntu-latest
    needs: setup
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Download shared decapod binary
        uses: actions/download-artifact@v8
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug
      - name: Make shared decapod binary executable
        run: chmod +x target/debug/decapod
      - name: Run chaos replay determinism test
        run: cargo test --test chaos_replay -- --test-threads=1

  # DAEMONLESS: ensure commands do not leave background processes
  daemonless-lifecycle:
    name: Daemonless Lifecycle Gate
    runs-on: ubuntu-latest
    needs: setup
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Download shared decapod binary
        uses: actions/download-artifact@v8
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug
      - name: Install shared decapod binary
        run: |
          chmod +x target/debug/decapod
          echo "$PWD/target/debug" >> "$GITHUB_PATH"
      - name: Run daemonless lifecycle test
        run: cargo test --test daemonless_lifecycle -- --test-threads=1

  # CONTRACTS: README/Constitution drift detection
  contracts:
    name: Contract Conformance
    runs-on: ubuntu-latest
    needs: setup
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Download shared decapod binary
        uses: actions/download-artifact@v8
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug
      - name: Make shared decapod binary executable
        run: chmod +x target/debug/decapod
      - name: Run contract conformance tests
        run: |
          cargo test --test contract_conformance -- --test-threads=1
          cargo test --test constitution_claims -- --test-threads=1
      - name: Run CLI contract enforcement tests
        run: cargo test --test cli_contract_enforcement -- --test-threads=1

  # MIGRATION TESTS: Only when SQL migration scripts change
  migration-script-tests:
    name: Migration Script Tests
    runs-on: ubuntu-latest
    needs: [setup, detect-migration-script-changes]
    if: github.event_name == 'pull_request' && needs.detect-migration-script-changes.outputs.changed == 'true'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "ci-build"
          save-if: "false"
      - name: Download shared decapod binary
        uses: actions/download-artifact@v8
        with:
          name: decapod-debug-linux-${{ github.sha }}
          path: target/debug
      - name: Make shared decapod binary executable
        run: chmod +x target/debug/decapod
      - name: Run migration tests
        run: cargo test --test core_tests migration_ -- --ignored --test-threads=1


  # RELEASE BUILD: Only on master, after all checks pass
  release-build:
    name: Release Build Check
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/master'
    needs: [fmt, clippy, test, test-rpc-suite, chaos-replay, daemonless-lifecycle, contracts, health]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          persist-credentials: false
      - name: Install LLD
        uses: ./.github/actions/install-lld
      - uses: dtolnay/rust-toolchain@stable
      - name: Restore cache
        uses: Swatinem/rust-cache@v2
        with:
          shared-key: "release"
          cache-targets: true
          save-if: true
      - name: Build release
        run: cargo build --release --locked
      - name: Upload binary
        uses: actions/upload-artifact@v7
        with:
          name: decapod-${{ github.sha }}
          path: target/release/decapod
          retention-days: 1
          compression-level: 9
