name: Rust

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

env:
  CARGO_TERM_COLOR: always

jobs:
  # Quick check: every sub-crate compiles independently with default features.
  # This catches broken inter-crate boundaries before the heavier coverage job
  # exercises the full umbrella.
  check_subcrates:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        crate:
          - stochastic-rs-core
          - stochastic-rs-distributions
          - stochastic-rs-stochastic
          - stochastic-rs-copulas
          - stochastic-rs-stats
          - stochastic-rs-quant
          - stochastic-rs-ai
          - stochastic-rs-viz
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
      - name: Cargo check (${{ matrix.crate }})
        run: cargo check -p ${{ matrix.crate }} --no-default-features

  # Umbrella + features matrix. Covers feature combinations that exercise
  # cross-crate cfg-gated code (notably the python+gpu Array2 overlap caught
  # by the 2026-05-07 audit §4.1).
  check_umbrella:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        features:
          - "" # default
          - "ai"
          - "openblas"
          - "yahoo"
          - "python"
          - "gpu"
          - "python,gpu"
          - "openblas,ai,yahoo"
    steps:
      - uses: actions/checkout@v3
      - name: Install OpenBLAS (when needed)
        if: contains(matrix.features, 'openblas')
        run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
      - uses: dtolnay/rust-toolchain@stable
      - name: Cargo check (features=${{ matrix.features }})
        run: |
          if [ -z "${{ matrix.features }}" ]; then
            cargo check -p stochastic-rs --all-targets
          else
            cargo check -p stochastic-rs --all-targets --features "${{ matrix.features }}"
          fi

  # Lint + format gates. Catches drift independently of the feature matrix.
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - name: cargo fmt --check
        run: cargo fmt --all -- --check
      - name: cargo clippy (workspace)
        run: cargo clippy --workspace --no-default-features --all-targets -- -D warnings

  # In-process Python smoke test (audit §7.9): builds the cdylib via
  # `maturin develop` and exercises the 5 main API surfaces (distributions,
  # stochastic, copulas, stats, quant) including seed-determinism for the
  # samplers that accept a `seed=` keyword.
  python_smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - name: Install OpenBLAS
        run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
      # `maturin develop` requires an active virtualenv on Linux; the
      # ubuntu-latest runner does not have one by default. Create one and
      # invoke maturin / python via the venv's binaries (no `source` so
      # each `run:` block — which is its own shell — still picks it up).
      - name: Set up venv + install build deps
        run: |
          python -m venv .venv
          .venv/bin/pip install --upgrade pip
          .venv/bin/pip install maturin numpy pytest scipy
      - name: maturin develop (openblas)
        run: .venv/bin/maturin develop --release --features openblas
      - name: Run python_smoke.py
        # `-u` flushes prints so the last `[OK]`/`[FAIL]` line before any
        # crash shows up in the log; `-X faulthandler` prints a C+Python
        # traceback on SIGSEGV, pinning the crashing native call.
        run: .venv/bin/python -X faulthandler -u stochastic-rs-py/python_smoke.py
      - name: Run pytest suite
        # The pytest suite (stochastic-rs-py/tests/) covers all five API
        # surfaces beyond the smoke script; conftest.py skips with a clear
        # message if the extension is somehow not importable.
        run: .venv/bin/python -m pytest stochastic-rs-py/tests/ -v

  # Fast correctness gate: the whole workspace test-suite via cargo-nextest
  # (no coverage instrumentation), so a failing/flaky test surfaces in minutes
  # instead of only inside the multi-hour coverage job. The `ci` nextest
  # profile prints one line per finished test with a running `(N/total)`
  # counter, so the log shows which test is executing and how far along the
  # run is.
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 90
    steps:
      - uses: actions/checkout@v3
      - name: Install OpenBLAS
        run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install nextest
        uses: taiki-e/install-action@v2
        with:
          tool: nextest
      # `--release` because the heavy Monte-Carlo tests (Malliavin, rBergomi,
      # Fourier-Malliavin) are unusably slow unoptimised; `--exclude
      # stochastic-rs-py` because the cdylib is exercised by pytest in the
      # python_smoke job. Mirrors the coverage job's feature set for parity.
      - name: cargo nextest run (workspace)
        run: cargo nextest run --workspace --exclude stochastic-rs-py --features openblas --profile ci
      # nextest does not run doctests; this gates the crates' doc examples
      # (e.g. the README quickstart rendered on docs.rs) that plain `cargo
      # test` used to cover. `--release` reuses the artifacts built above.
      - name: cargo test --doc (workspace)
        run: cargo test --release --workspace --exclude stochastic-rs-py --features openblas --doc

  # Full test run with openblas + coverage. Heavyweight + instrumented, so it
  # only runs on pushes to main; the fast `test` job gates pull requests.
  test_with_coverage:
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    timeout-minutes: 240

    steps:
      - uses: actions/checkout@v3

      - name: Install system dependencies
        run: sudo apt-get update && sudo apt-get install -y libopenblas-dev

      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: llvm-tools-preview

      - name: Add llvm-tools to PATH
        run: echo "${HOME}/.rustup/toolchains/$(rustup show active-toolchain | cut -d' ' -f1)/bin" >> $GITHUB_PATH

      - name: Install grcov
        run: cargo install grcov

      - name: Install nextest
        uses: taiki-e/install-action@v2
        with:
          tool: nextest

      - name: Clean Build Artifacts
        run: cargo clean

      - name: Run tests with coverage
        env:
          CARGO_INCREMENTAL: "0"
          CARGO_PROFILE_RELEASE_LTO: "false"
          CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
          CARGO_PROFILE_RELEASE_DEBUG: "1"
          RUSTFLAGS: "-C instrument-coverage"
          LLVM_PROFILE_FILE: "coverage-%p-%m.profraw"
        # nextest runs each test in its own process (one profraw per test, kept
        # unique by the %p pid above) and the `ci-coverage` profile prints the
        # running (N/total) counter; grcov then merges every profraw as before.
        run: |
          cargo nextest run --release --workspace --exclude stochastic-rs-py --features openblas --profile ci-coverage

      - name: Generate coverage report
        run: |
          grcov . \
            --binary-path ./target/release/ \
            -s . \
            -t lcov \
            --branch \
            --ignore-not-existing \
            --llvm \
            -o lcov.info
          # nextest writes one profraw per test (~1300 files, >1 GB total).
          # grcov has merged them into lcov.info above, so delete them — else
          # the Codecov upload sweeps up a gigabyte of raw profiling data
          # instead of the single lcov report, which Codecov cannot parse.
          find . -name '*.profraw' -delete

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          # Upload only lcov.info; do not let the CLI discover/attach anything
          # else in the tree (belt-and-braces with the profraw cleanup above).
          disable_search: true
          fail_ci_if_error: true
          verbose: true
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
