name: CI

on:
  push:
    branches: ["**"]
  pull_request:
  workflow_dispatch:
    inputs:
      skip_integration:
        description: "Skip integration tests"
        type: boolean
        default: false

permissions:
  checks: write
  contents: read

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  SOURCE_DATE_EPOCH: 0  # overridden per-step after checkout

# ─────────────────────────────────────────────────────────────────────────────
# Job 1 – Build matrix
#
# Builds on Linux x86_64 and Linux aarch64.  macOS and Windows are in a
# separate ci-compat.yml workflow so their failures don't mark this run red.
# ─────────────────────────────────────────────────────────────────────────────
jobs:
  fmt:
    name: Format check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - run: cargo fmt --check

  build:
    name: Build (${{ matrix.os }})
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
          - os: ubuntu-24.04-arm

    steps:
      - uses: actions/checkout@v4

      - name: Set SOURCE_DATE_EPOCH from git
        run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Cargo registry + build
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Build
        run: cargo build --release

      - name: SHA-256 hashes
        run: sha256sum target/release/fips target/release/fipsctl target/release/fipstop

      # Upload the Linux binary so integration jobs can use it without rebuilding
      - name: Upload Linux binary
        if: matrix.os == 'ubuntu-latest'
        uses: actions/upload-artifact@v4
        with:
          name: fips-linux
          path: |
            target/release/fips
            target/release/fipsctl
            target/release/fipstop
          retention-days: 1

# ─────────────────────────────────────────────────────────────────────────────
# Job 2 – Unit tests
#
# Runs `cargo test` on Linux.  Gated on the build matrix completing so we
# don't waste runner time if compilation is broken.
# ─────────────────────────────────────────────────────────────────────────────
  test:
    name: Unit tests
    runs-on: ubuntu-latest
    needs: [build]
    steps:
      - uses: actions/checkout@v4

      - name: Set SOURCE_DATE_EPOCH from git
        run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Cargo registry + build
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Install cargo-nextest
        uses: taiki-e/install-action@nextest

      - name: Run unit tests
        run: cargo nextest run --all --profile ci

      - name: Publish test report (Checks tab)
        uses: dorny/test-reporter@v2
        if: always()
        with:
          name: Unit Tests
          path: target/nextest/ci/junit.xml
          reporter: java-junit
          fail-on-error: false

      - name: Publish test report (run summary)
        uses: mikepenz/action-junit-report@v4
        if: always()
        with:
          report_paths: target/nextest/ci/junit.xml
          check_name: Unit Tests Summary
          fail_on_failure: false

# ─────────────────────────────────────────────────────────────────────────────
# Job 3 – Integration tests (static mesh + chaos simulation)
#
# Runs only when both build and test succeed.  Each topology / scenario is a
# separate matrix entry so they run in parallel.
#
# All harnesses share a single Docker image (fips-test:latest) built once
# in the setup step from testing/docker/.
# ─────────────────────────────────────────────────────────────────────────────
  integration:
    name: Integration (${{ matrix.suite }})
    runs-on: ubuntu-latest
    needs: [build, test]
    if: ${{ !inputs.skip_integration }}

    strategy:
      fail-fast: false
      matrix:
        include:
          # ── Static mesh topologies ─────────────────────────────────────────
          - suite: static-mesh
            type: static
            topology: mesh
          - suite: static-chain
            type: static
            topology: chain
          # ── Rekey integration test ──────────────────────────────────────────
          - suite: rekey
            type: rekey
            topology: rekey
          # ── Chaos / stochastic scenarios ───────────────────────────────────
          - suite: chaos-smoke-10
            type: chaos
            scenario: smoke-10
          - suite: churn-mixed-10
            type: chaos
            scenario: churn-mixed
            chaos_flags: "--nodes 10 --duration 120"
          - suite: ethernet-mesh
            type: chaos
            scenario: ethernet-mesh
          - suite: ethernet-only
            type: chaos
            scenario: ethernet-only
          - suite: tcp-mesh
            type: chaos
            scenario: tcp-mesh
          - suite: bottleneck-parent
            type: chaos
            scenario: bottleneck-parent
          - suite: cost-avoidance
            type: chaos
            scenario: cost-avoidance
          - suite: cost-reeval
            type: chaos
            scenario: cost-reeval
          - suite: cost-stability
            type: chaos
            scenario: cost-stability
          - suite: depth-vs-cost
            type: chaos
            scenario: depth-vs-cost
          - suite: mixed-technology
            type: chaos
            scenario: mixed-technology
          - suite: congestion-stress
            type: chaos
            scenario: congestion-stress
          # ── Sidecar deployment ──────────────────────────────────────────
          - suite: sidecar
            type: sidecar

    steps:
      - uses: actions/checkout@v4

      # Fetch the pre-built Linux binary from job 1
      - name: Download Linux binary
        uses: actions/download-artifact@v4
        with:
          name: fips-linux
          path: _bin

      # Install binaries to unified docker context and build shared image
      - name: Install binaries and build Docker image
        run: |
          chmod +x _bin/fips _bin/fipsctl
          [ -f _bin/fipstop ] && chmod +x _bin/fipstop || true
          cp _bin/fips    testing/docker/fips
          cp _bin/fipsctl testing/docker/fipsctl
          [ -f _bin/fipstop ] && cp _bin/fipstop testing/docker/fipstop || true
          docker build -t fips-test:latest testing/docker
          docker build -t fips-test-app:latest -f testing/docker/Dockerfile.app testing/docker

      # ── Static topology ────────────────────────────────────────────────────
      - name: Generate configs (static)
        if: matrix.type == 'static'
        run: bash testing/static/scripts/generate-configs.sh ${{ matrix.topology }}

      - name: Start containers (static)
        if: matrix.type == 'static'
        run: |
          docker compose -f testing/static/docker-compose.yml \
            --profile ${{ matrix.topology }} up -d

      - name: Run ping test (static)
        if: matrix.type == 'static'
        run: bash testing/static/scripts/ping-test.sh ${{ matrix.topology }}

      - name: Collect logs on failure (static)
        if: matrix.type == 'static' && failure()
        run: |
          docker compose -f testing/static/docker-compose.yml \
            --profile ${{ matrix.topology }} logs --no-color

      - name: Stop containers (static)
        if: matrix.type == 'static' && always()
        run: |
          docker compose -f testing/static/docker-compose.yml \
            --profile ${{ matrix.topology }} down --volumes --remove-orphans

      # ── Rekey integration test ──────────────────────────────────────────────
      - name: Generate and inject configs (rekey)
        if: matrix.type == 'rekey'
        run: |
          bash testing/static/scripts/generate-configs.sh rekey
          bash testing/static/scripts/rekey-test.sh inject-config

      - name: Start containers (rekey)
        if: matrix.type == 'rekey'
        run: |
          docker compose -f testing/static/docker-compose.yml \
            --profile rekey up -d

      - name: Run rekey test
        if: matrix.type == 'rekey'
        run: bash testing/static/scripts/rekey-test.sh

      - name: Collect logs on failure (rekey)
        if: matrix.type == 'rekey' && failure()
        run: |
          docker compose -f testing/static/docker-compose.yml \
            --profile rekey logs --no-color

      - name: Stop containers (rekey)
        if: matrix.type == 'rekey' && always()
        run: |
          docker compose -f testing/static/docker-compose.yml \
            --profile rekey down --volumes --remove-orphans

      # ── Chaos simulation ───────────────────────────────────────────────────
      - name: Install Python deps (chaos)
        if: matrix.type == 'chaos'
        run: pip3 install --quiet pyyaml jinja2

      - name: Run chaos scenario
        if: matrix.type == 'chaos'
        run: bash testing/chaos/scripts/chaos.sh ${{ matrix.scenario }} ${{ matrix.chaos_flags }}

      - name: Upload sim results on failure (chaos)
        if: matrix.type == 'chaos' && failure()
        uses: actions/upload-artifact@v4
        with:
          name: sim-results-${{ matrix.scenario }}
          path: testing/chaos/sim-results/
          retention-days: 7

      # ── Sidecar deployment ──────────────────────────────────────────────
      - name: Run sidecar integration test
        if: matrix.type == 'sidecar'
        run: bash testing/sidecar/scripts/test-sidecar.sh --skip-build

      - name: Collect logs on failure (sidecar)
        if: matrix.type == 'sidecar' && failure()
        run: |
          for node in a b c; do
            echo "--- sidecar-${node} logs ---"
            docker logs "sidecar-${node}-fips-1" 2>&1 || true
            echo ""
          done
