name: CI

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

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-Dwarnings"

jobs:
  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - name: Check formatting
        run: |
          set -o pipefail
          cargo fmt --all -- --check 2>&1 | tee fmt-output.txt
      - name: Upload fmt results
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: fmt-results
          path: fmt-output.txt
          retention-days: 5

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - name: Run clippy
        run: |
          set -o pipefail
          cargo clippy --all-targets --all-features --message-format=json 2>&1 | tee clippy-output.json
      - name: Upload clippy results
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: clippy-results
          path: clippy-output.json
          retention-days: 5

  test:
    name: Tests (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: taiki-e/install-action@nextest
      - name: Run tests
        shell: bash
        run: |
          set -o pipefail
          cargo nextest run --all-targets --all-features 2>&1 | tee test-output.log
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: test-results-${{ matrix.os }}
          path: test-output.log
          retention-days: 5

  build:
    name: Build (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Build release
        shell: bash
        run: |
          set -o pipefail
          cargo build --release --message-format=json 2>&1 | tee build-output.json
      - name: Upload build results
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: build-results-${{ matrix.os }}
          path: build-output.json
          retention-days: 5

  integration:
    name: Integration Tests (${{ matrix.db_name }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - db_name: scylladb-2026.1
            db_image: scylladb/scylla:2026.1
            db_args: "--smp 1 --memory 512M --overprovisioned 1 --skip-wait-for-gossip-to-settle 0"
            db_env: ""
          - db_name: scylladb-2025.1
            db_image: scylladb/scylla:2025.1
            db_args: "--smp 1 --memory 512M --overprovisioned 1 --skip-wait-for-gossip-to-settle 0"
            db_env: ""
          - db_name: cassandra-4.1
            db_image: cassandra:4.1
            db_args: ""
            db_env: "-e CASSANDRA_DC=datacenter1 -e JVM_EXTRA_OPTS=-Dcassandra.enable_materialized_views=true"
          - db_name: cassandra-5.0
            db_image: cassandra:5.0
            db_args: ""
            db_env: "-e CASSANDRA_DC=datacenter1 -e JVM_EXTRA_OPTS=-Dcassandra.enable_materialized_views=true"
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      # Temporary workaround: testcontainers-rs fails to start containers on
      # GitHub Actions (Ubuntu 24.04 / Docker 27) due to Docker networking driver
      # failing with "address already in use" when setting up port forwarding.
      # Goal: restore plain `cargo test` with testcontainers once root cause is fixed.
      # Tracked: https://github.com/scylladb/cqlsh-rs/issues/TODO
      - name: Start database (${{ matrix.db_name }})
        run: |
          docker run -d --name testdb \
            -p 9042:9042 \
            ${{ matrix.db_env }} \
            ${{ matrix.db_image }} \
            ${{ matrix.db_args }}
          echo "Waiting for ${{ matrix.db_name }} to be ready..."
          for i in $(seq 1 60); do
            if docker exec testdb cqlsh -e "SELECT now() FROM system.local;" 2>/dev/null; then
              echo "${{ matrix.db_name }} ready after ${i} attempt(s)"
              break
            fi
            if [ "$i" -eq 60 ]; then
              echo "${{ matrix.db_name }} failed to become ready"
              docker logs testdb
              exit 1
            fi
            sleep 5
          done
      - name: Install cargo-tarpaulin
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-tarpaulin
      - name: Run integration tests
        env:
          SCYLLA_TEST_HOST: localhost
          SCYLLA_TEST_PORT: "9042"
        run: |
          set -o pipefail
          cargo tarpaulin \
            --all-features \
            --workspace \
            --timeout 300 \
            --test integration \
            --out xml \
            --output-dir coverage-integration/ \
            --skip-clean \
            -- --ignored --test-threads=1 2>&1 | tee integration-output.log
        timeout-minutes: 25
      - name: Upload integration coverage to Codecov
        uses: codecov/codecov-action@v5
        with:
          files: coverage-integration/cobertura.xml
          flags: integration-${{ matrix.db_name }}
          fail_ci_if_error: false
          token: ${{ secrets.CODECOV_TOKEN }}
      - name: Upload integration test results
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: integration-test-results-${{ matrix.db_name }}
          path: integration-output.log
          retention-days: 5

  coverage:
    name: Code Coverage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          key: coverage
      - name: Install cargo-tarpaulin
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-tarpaulin
      - name: Run unit test coverage
        run: |
          cargo tarpaulin \
            --all-features \
            --workspace \
            --timeout 120 \
            --out xml \
            --output-dir coverage/ \
            --skip-clean \
            --lib --bins
        timeout-minutes: 20
      - name: Upload unit coverage to Codecov
        uses: codecov/codecov-action@v5
        with:
          files: coverage/cobertura.xml
          flags: unittests
          fail_ci_if_error: false
          token: ${{ secrets.CODECOV_TOKEN }}
      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v6
        with:
          name: coverage-report
          path: coverage/
          retention-days: 30

  # ===========================================================================
  # Automated release pipeline
  #
  # Similar to scylladb/coodie's release automation (PR #220), this uses
  # release-plz to detect version bumps from conventional commits and
  # automate tagging, changelog, crates.io publishing, cross-platform
  # binary builds, and Docker image publishing.
  #
  # Flow:
  #   1. release-plz release-pr  → creates/updates a "Release vX.Y.Z" PR
  #   2. Maintainer merges the release PR (bumps Cargo.toml + CHANGELOG.md)
  #   3. release-plz release     → creates git tag, GitHub Release, publishes
  #      to crates.io
  #   4. Downstream jobs build cross-platform binaries, upload them to the
  #      GitHub Release, and push a multi-arch Docker image.
  #
  # Token setup:
  #   The release-pr step needs a token with permission to create PRs.
  #   The default GITHUB_TOKEN is blocked by org/repo settings, so we
  #   use the RELEASE_PLZ_TOKEN secret (a PAT with repo scope).
  #   To set it up:
  #     1. Create a fine-grained PAT with Contents + Pull Requests read/write
  #     2. Add it as a repository secret named RELEASE_PLZ_TOKEN
  # ===========================================================================
  release:
    name: Release
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    needs: [fmt, clippy, test, build]

    permissions:
      contents: write
      pull-requests: write

    concurrency:
      group: release-${{ github.ref_name }}
      cancel-in-progress: false

    outputs:
      releases_created: ${{ steps.release.outputs.releases_created }}
      version: ${{ steps.extract.outputs.version }}
      tag: ${{ steps.extract.outputs.tag }}

    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - uses: dtolnay/rust-toolchain@stable

      # Create/update a release PR with version bump + CHANGELOG.
      # Uses RELEASE_PLZ_TOKEN (PAT) because the default GITHUB_TOKEN
      # is not permitted to create PRs in this repo.
      - name: Release PR
        uses: release-plz/action@v0.5
        with:
          command: release-pr
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}

      # If Cargo.toml version > latest tag, create tag + GitHub Release + publish to crates.io
      - name: Release
        id: release
        uses: release-plz/action@v0.5
        with:
          command: release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

      - name: Extract release info
        id: extract
        if: steps.release.outputs.releases_created == 'true'
        run: |
          TAG=$(echo '${{ steps.release.outputs.releases }}' | jq -r '.[0].tag')
          VERSION=$(echo '${{ steps.release.outputs.releases }}' | jq -r '.[0].version')
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "::notice::Released ${TAG} (${VERSION})"

  # ---------------------------------------------------------------------------
  # Build matrix: cross-compile release binaries for all targets.
  # Only runs when a new version is released.
  # ---------------------------------------------------------------------------
  build-release-binaries:
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    if: needs.release.outputs.releases_created == 'true'
    needs: [release]

    permissions:
      contents: read

    env:
      # Override workflow-level RUSTFLAGS — release cross-compilation targets
      # may produce different warnings than the CI build targets.
      RUSTFLAGS: ""

    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            cross: false
            artifact: cqlsh-rs
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
            cross: true
            artifact: cqlsh-rs
          - target: x86_64-apple-darwin
            os: macos-14
            cross: false
            artifact: cqlsh-rs
          - target: aarch64-apple-darwin
            os: macos-14
            cross: false
            artifact: cqlsh-rs
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            cross: false
            artifact: cqlsh-rs.exe

    steps:
      - uses: actions/checkout@v5
        with:
          ref: ${{ needs.release.outputs.tag }}

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          key: release-${{ matrix.target }}

      - name: Install musl-tools
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get update && sudo apt-get install -y musl-tools

      - name: Install cross
        if: matrix.cross
        run: cargo install cross --git https://github.com/cross-rs/cross

      - name: Build release binary
        shell: bash
        run: |
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --target ${{ matrix.target }}
          else
            cargo build --release --target ${{ matrix.target }}
          fi

      - name: Package binary
        shell: bash
        run: |
          VERSION="${{ needs.release.outputs.version }}"
          ARCHIVE_NAME="cqlsh-rs-${VERSION}-${{ matrix.target }}"

          mkdir -p "staging/${ARCHIVE_NAME}"
          cp "target/${{ matrix.target }}/release/${{ matrix.artifact }}" "staging/${ARCHIVE_NAME}/"
          cp README.md LICENSE "staging/${ARCHIVE_NAME}/" 2>/dev/null || true

          cd staging
          if [ "${{ matrix.os }}" = "windows-latest" ]; then
            7z a "../${ARCHIVE_NAME}.zip" "${ARCHIVE_NAME}"
            echo "ASSET_PATH=${ARCHIVE_NAME}.zip" >> "$GITHUB_ENV"
          else
            tar czf "../${ARCHIVE_NAME}.tar.gz" "${ARCHIVE_NAME}"
            echo "ASSET_PATH=${ARCHIVE_NAME}.tar.gz" >> "$GITHUB_ENV"
          fi

      - name: Upload build artifact
        uses: actions/upload-artifact@v6
        with:
          name: ${{ matrix.target }}
          path: ${{ env.ASSET_PATH }}
          retention-days: 5

  # ---------------------------------------------------------------------------
  # Generate man pages and shell completions for the release
  # ---------------------------------------------------------------------------
  generate-release-assets:
    name: Generate Release Assets
    runs-on: ubuntu-latest
    if: needs.release.outputs.releases_created == 'true'
    needs: [release]

    permissions:
      contents: read

    env:
      RUSTFLAGS: ""

    steps:
      - uses: actions/checkout@v5
        with:
          ref: ${{ needs.release.outputs.tag }}

      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: Build binary
        run: cargo build --release

      - name: Generate man page
        run: |
          mkdir -p assets
          ./target/release/cqlsh-rs --generate-man > assets/cqlsh-rs.1

      - name: Generate shell completions
        run: |
          mkdir -p assets/completions
          ./target/release/cqlsh-rs --completions bash > assets/completions/cqlsh-rs.bash
          ./target/release/cqlsh-rs --completions zsh > assets/completions/_cqlsh-rs
          ./target/release/cqlsh-rs --completions fish > assets/completions/cqlsh-rs.fish

      - name: Package assets
        run: tar czf assets.tar.gz -C assets .

      - name: Upload assets
        uses: actions/upload-artifact@v6
        with:
          name: assets
          path: assets.tar.gz
          retention-days: 5

  # ---------------------------------------------------------------------------
  # Upload release artifacts (binaries + checksums) to the GitHub Release
  # created by release-plz
  # ---------------------------------------------------------------------------
  upload-release-artifacts:
    name: Upload Release Artifacts
    runs-on: ubuntu-latest
    if: needs.release.outputs.releases_created == 'true'
    needs: [release, build-release-binaries, generate-release-assets]

    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v5

      - name: Download release artifacts
        uses: actions/download-artifact@v6
        with:
          path: artifacts
          pattern: "{x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,x86_64-apple-darwin,aarch64-apple-darwin,x86_64-pc-windows-msvc,assets}"

      - name: Prepare release files
        run: |
          mkdir -p release
          for dir in artifacts/x86_64-unknown-linux-musl \
                     artifacts/aarch64-unknown-linux-musl \
                     artifacts/x86_64-apple-darwin \
                     artifacts/aarch64-apple-darwin \
                     artifacts/x86_64-pc-windows-msvc; do
            if [ -d "$dir" ]; then
              cp "$dir"/* release/
            fi
          done
          # Extract and include assets (man page, completions)
          mkdir -p release/assets
          tar xzf artifacts/assets/assets.tar.gz -C release/assets
          cd release
          tar czf cqlsh-rs-man-and-completions.tar.gz -C assets .
          rm -rf assets

      - name: Generate SHA256 checksums
        run: |
          cd release
          sha256sum * > SHA256SUMS.txt
          cat SHA256SUMS.txt

      - name: Upload to GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.release.outputs.tag }}
          files: release/*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # ---------------------------------------------------------------------------
  # Build and push multi-arch Docker image
  # ---------------------------------------------------------------------------
  docker-release:
    name: Docker Image (multi-arch)
    runs-on: ubuntu-latest
    if: needs.release.outputs.releases_created == 'true'
    needs: [release, build-release-binaries]

    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v5

      - name: Download Linux x86_64 binary
        uses: actions/download-artifact@v6
        with:
          name: x86_64-unknown-linux-musl
          path: artifacts/amd64

      - name: Download Linux aarch64 binary
        uses: actions/download-artifact@v6
        with:
          name: aarch64-unknown-linux-musl
          path: artifacts/arm64

      - name: Extract binaries
        run: |
          mkdir -p docker-build
          cd artifacts/amd64
          tar xzf *.tar.gz
          find . -name "cqlsh-rs" -type f -exec cp {} ../../docker-build/cqlsh-rs-amd64 \;
          cd ../arm64
          tar xzf *.tar.gz
          find . -name "cqlsh-rs" -type f -exec cp {} ../../docker-build/cqlsh-rs-arm64 \;
          cd ../..
          chmod +x docker-build/cqlsh-rs-*

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/${{ github.repository }}
            docker.io/scylladb/cqlsh-rs
          tags: |
            type=raw,value=${{ needs.release.outputs.version }}
            type=raw,value=latest

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  # ---------------------------------------------------------------------------
  # Build Linux musl binaries for Docker (runs on every main push)
  # ---------------------------------------------------------------------------
  build-docker-binaries:
    name: Build Docker ${{ matrix.target }}
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    needs: [fmt, clippy, test, build, integration]

    permissions:
      contents: read

    env:
      RUSTFLAGS: ""

    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            cross: false
          - target: aarch64-unknown-linux-musl
            cross: true

    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          key: docker-${{ matrix.target }}

      - name: Install musl-tools
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get update && sudo apt-get install -y musl-tools

      - name: Install cross
        if: matrix.cross
        run: cargo install cross --git https://github.com/cross-rs/cross

      - name: Build release binary
        shell: bash
        run: |
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --target ${{ matrix.target }}
          else
            cargo build --release --target ${{ matrix.target }}
          fi

      - name: Package binary
        run: |
          mkdir -p staging
          cp "target/${{ matrix.target }}/release/cqlsh-rs" staging/
          cd staging
          tar czf "../cqlsh-rs-${{ matrix.target }}.tar.gz" cqlsh-rs

      - name: Upload build artifact
        uses: actions/upload-artifact@v6
        with:
          name: docker-${{ matrix.target }}
          path: cqlsh-rs-${{ matrix.target }}.tar.gz
          retention-days: 5

  # ---------------------------------------------------------------------------
  # Push 'main' tag Docker image on every main push
  # ---------------------------------------------------------------------------
  docker-main:
    name: Docker Image (main)
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    needs: [build-docker-binaries]

    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v5

      - name: Download Linux x86_64 binary
        uses: actions/download-artifact@v6
        with:
          name: docker-x86_64-unknown-linux-musl
          path: artifacts/amd64

      - name: Download Linux aarch64 binary
        uses: actions/download-artifact@v6
        with:
          name: docker-aarch64-unknown-linux-musl
          path: artifacts/arm64

      - name: Extract binaries
        run: |
          mkdir -p docker-build
          cd artifacts/amd64
          tar xzf *.tar.gz
          cp cqlsh-rs ../../docker-build/cqlsh-rs-amd64
          cd ../arm64
          tar xzf *.tar.gz
          cp cqlsh-rs ../../docker-build/cqlsh-rs-arm64
          cd ../..
          chmod +x docker-build/cqlsh-rs-*

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/${{ github.repository }}
            docker.io/scylladb/cqlsh-rs
          tags: |
            type=raw,value=main

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
