name: CI

on:
  workflow_call:
  pull_request:
  push:

defaults:
  run:
    shell: bash

env:
  RELEASE: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/test-v') }}
  # The test-installer job can use an old existing image rather than the most
  # recently built image; the 'needs' can be commented out to allow the
  # test-installer job to run independently (and therefore faster)
  INSTALLER_USE_OLD_IMAGE: false
  INSTALLER_OLD_RUNID: 3204825457
  # The test-installer job can run with the pyinstaller debug bundle
  INSTALLER_USE_DEBUG: false
  BRANCH_NAME: ${{ github.head_ref || github.ref_name }}

jobs:

  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: astral-sh/ruff-action@v3
        with:
          version: "latest"
          args: "check --fix-only --exit-non-zero-on-fix"
      - uses: pre-commit-ci/lite-action@v1.1.0
        if: always()
        with:
          msg: '[pre-commit.ci lite] apply automatic fixes for ruff linting errors'

  matrix:
    needs: [ruff]
    name: Generate test matrix

    #if: false    # uncomment to prevent the rest of the workflow running

    runs-on: ubuntu-latest

    # Stop existing workflows for matching branches and pull requests
    concurrency:
      group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
      cancel-in-progress: true

    outputs:
      matrix-json: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: 3.11

      - uses: actions/checkout@v5
      - id: set-matrix
        run: python .github/workflows/matrix.py >> $GITHUB_OUTPUT


  builddeps:
    needs: [ruff]
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        package: [
          ["sasdata", "sasdata"],
          ["sasmodels", "sasmodels"]
        ]

    name: Build wheel "${{ matrix.package[0] }}"

    steps:
      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: 3.11

      ### Compilation of build-dependencies

      - name: Build wheel of ${{ matrix.package[0] }}
        run: |
          python -m pip wheel --verbose ${{ matrix.package[1] }}

      - name: Store wheel for use
        uses: actions/upload-artifact@v4
        with:
          name: sasview-wheel-${{ matrix.package[0] }}
          path: |
            ${{ matrix.package[0] }}*whl
          if-no-files-found: ignore

  build-matrix:
    needs: [matrix, builddeps]

    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJson(needs.matrix.outputs.matrix-json) }}
    env:
      MACOS_DMG_NAME: SasView6-${{ matrix.os }}.dmg
      # Only sign/notarize if the secrets will be available
      RELEASE_BRANCH_BUILD: ${{
          (
            (github.event_name == 'push' && contains(github.ref, 'release')) ||
            (github.event_name == 'push' && contains(github.ref, 'tags/v6.1')) ||
            (github.event_name == 'pull_request' && contains(github.base_ref, 'release-6.1'))
          ) && 'true' || '' }}

      # Only sign the installer if non-forked repo
      SIGN_INSTALLER: ${{ (! github.event.repository.fork && ! github.event.pull_request.head.repo.fork ) && 'true' || '' }}

      UV_SYSTEM_PYTHON: 1

    name: "${{ matrix.job_name }}"

    runs-on: ${{ matrix.os }}

    steps:
      - name: Configuration of this job
        run: |
          echo '${{ toJson(matrix) }}'

      - name: Obtain SasView source from git
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install uv utilities to speed up python module installation
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true
          prune-cache: false

      ### Installation of build-dependencies

      - name: Install X11 libraries (Linux)
        if: ${{ startsWith(matrix.os, 'ubuntu') }}
        env:
          DEBIAN_FRONTEND: noninteractive
        run: |
          sudo apt-get update
          sudo apt-get -y install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils xvfb libegl-dev libcairo2-dev

      - name: Fetch wheels of sibling projects
        uses: actions/download-artifact@v5
        with:
          path: dist/
          pattern: sasview-wheel-*
          merge-multiple: true

      - name: Install correct versions of sibling projects
        run: |
          uv pip install dist/*whl

      - name: Install Python dependencies
        if: ${{ ! env.RELEASE_BRANCH_BUILD }}
        run: |
          uv pip install -r build_tools/requirements.txt
          uv pip install -r build_tools/requirements-dev.txt

      ### For pull_requests with the release branch as the base -or- branches with the name *release*

      - name: Install Python dependencies for release branches
        if: ${{ env.RELEASE_BRANCH_BUILD }}
        run: |
          uv pip install --no-deps -r build_tools/requirements-release-${{ matrix.os }}.txt

      ### Actual building of sasview

      - name: Build sasview
        run: |
          python -m build --no-isolation --sdist --wheel

      - name: Install sasview wheel
        run: |
          uv pip install dist/sasview*whl

      - name: Publish sdist and wheel package
        uses: actions/upload-artifact@v4
        if: ${{ matrix.installer && startsWith(matrix.os, 'ubuntu') }}
        with:
          name: sasview-wheel
          path: |
            dist/sasview*whl
            dist/sasview*tar*
          if-no-files-found: ignore

      ### Run tests (if enabled)

      - name: Install test dependencies
        if: ${{ matrix.tests }}
        run: |
          uv pip install -r build_tools/requirements-test.txt

      - name: Test with pytest
        if: ${{ matrix.tests }}
        env:
          PYOPENCL_COMPILER_OUTPUT: 1
        run: |
          python -m pytest -v -s test

      # - name: Test GUI (Linux)
      #   if: ${{ matrix.tests && matrix.os == 'ubuntu-latest' }}
      #   env:
      #     PYOPENCL_COMPILER_OUTPUT: 1
      #   run: |
      #     # Suppress SIGSEGV from the tests until they can be fixed
      #     retval=0
      #     xvfb-run -a --server-args="-screen 0 1600x900x24" python -m pytest -rsx -v src/sas/qtgui/ || retval=$?
      #     if [ $retval -eq 139 ]; then echo "WARNING: Python interpreter exited with Segmentation Fault. This normally indicates that Qt objects were not correctly deleted. This error is currently suppressed in SasView's test suite."; retval=0; fi
      #     exit $retval


      ### Build the installer (if enabled)

      - name: Install utilities to build installer
        if: ${{ matrix.installer }}
        run: |
          uv pip install pyinstaller

      ### Document the build environment

      - name: Python package version list
        run: |
          python -m pip freeze

      - name: Build sasview with pyinstaller
        if: ${{ matrix.installer }}
        run: |
          cd installers
          rm -rf build/ dist/
          mkdir -p dist
          pyinstaller sasview.spec
          cd dist
          # the following builds a symlink farm in the package; it should
          # not be necessary, but without it, there will be lots of errors
          # about .so not found under linux.
          command -v ldconfig >/dev/null 2>&1 && ldconfig -n sasview
          tar zcf sasview-pyinstaller-dist.tar.gz sasview

      - name: Collect a debug tarball of the installer package
        if: ${{ matrix.installer }}
        uses: actions/upload-artifact@v4
        with:
          name: Debug-SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }}
          path: |
            installers/dist/sasview-pyinstaller-dist.tar.gz
          if-no-files-found: ignore

      - name: Install OS-specific installer tools (Windows)
        if: ${{ matrix.installer && startsWith(matrix.os, 'windows') }}
        run: |
          choco install innosetup

      - name: Build sasview installer with INNO (Windows)
        if: ${{ matrix.installer && startsWith(matrix.os, 'windows') }}
        run: |
          iscc installers/installer.iss
          mv installers/Output/setupSasView.exe installers/dist

      - name: Build sasview installer dmg file (OSX)
        if: ${{ matrix.installer && startsWith(matrix.os, 'macos') }}
        run: |
          cd installers/dist
          hdiutil create $MACOS_DMG_NAME -srcfolder SasView6.app -ov -format UDZO

      - name: Build sasview installer tarball (Linux)
        if: ${{ matrix.installer && startsWith(matrix.os, 'ubuntu') }}
        run: |
          cd installers/dist
          tar zcf SasView6.tar.gz sasview

      - name: Sign executable and create dmg (OSX)
        if: ${{ matrix.installer && startsWith(matrix.os, 'macos') && env.SIGN_INSTALLER }}
        env:
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE_ISA }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_ISA_PWD }}
        run: |
          echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
          security create-keychain -p DloaAcYP build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p DloaAcYP build.keychain
          security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k DloaAcYP build.keychain
          security find-identity -p codesigning
          
          cd installers/dist
          python ../../build_tools/fix_qt_folder_names_for_codesign.py SasView6.app
          python  ../../build_tools/code_sign_osx.py
          codesign --verify --options=runtime --entitlements ../../build_tools/entitlements.plist --timestamp --deep --verbose=4 --force --sign "Developer ID Application: The International Scattering Alliance (8CX8K63BQM)" SasView6.app
          hdiutil create $MACOS_DMG_NAME -srcfolder SasView6.app -ov -format UDZO
          codesign -s "Developer ID Application: The International Scattering Alliance (8CX8K63BQM)" $MACOS_DMG_NAME

      - name: Notarize Release Build (OSX)
        if: ${{ matrix.installer && startsWith(matrix.os, 'macos') && env.SIGN_INSTALLER }}
        uses: lando/notarize-action@v2
        with:
          product-path: installers/dist/SasView6-${{ matrix.os }}.dmg
          primary-bundle-id: "org.sasview.SasView6"
          appstore-connect-username: ${{ secrets.NOTARIZATION_USERNAME }}
          appstore-connect-password: ${{ secrets.NOTARIZATION_PASSWORD }}
          appstore-connect-team-id: 8CX8K63BQM
          verbose: True

      - name: Staple Release Build (OSX)
        if: ${{ matrix.installer && startsWith(matrix.os, 'macos') && env.SIGN_INSTALLER }}
        uses: BoundfoxStudios/action-xcode-staple@v1
        with:
          product-path: installers/dist/SasView6-${{ matrix.os }}.dmg

      - name: Install DigiCert Client tools from Github Custom Actions marketplace
        if: ${{ matrix.installer && startsWith(matrix.os, 'windows') && env.SIGN_INSTALLER }}
        uses: digicert/ssm-code-signing@v1.1.1

      - name: Set up P12 certificate
        if: ${{ matrix.installer && startsWith(matrix.os, 'windows') && env.SIGN_INSTALLER }}
        run: |
          echo "${{ secrets.WINDOZE_CERT_DATA }}" | base64 --decode > /d/Certificate_pkcs12.p12
        shell: bash

      - name: Set keylocker variables
        if: ${{ matrix.installer && startsWith(matrix.os, 'windows') && env.SIGN_INSTALLER }}
        id: variables
        run: |
          echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
          echo "SM_HOST=${{ secrets.KEYLOCKER_HOST }}" >> "$GITHUB_ENV"
          echo "SM_API_KEY=${{ secrets.KEYLOCKER_API_KEY }}" >> "$GITHUB_ENV"
          echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
          echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.WINDOZE_CERT_PASSWORD }}" >> "$GITHUB_ENV"
        shell: bash

      - name: Sign the binary using keypair alias
        if: ${{ matrix.installer && startsWith(matrix.os, 'windows') && env.SIGN_INSTALLER && env.RELEASE_BRANCH_BUILD }}
        run: |
           smctl sign --keypair-alias key_1369544803 --input installers/dist/setupSasView.exe
        shell: cmd

      - name: Publish installer package
        if: ${{ matrix.installer }}
        uses: actions/upload-artifact@v4
        with:
          name: SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }}
          path: |
            installers/dist/setupSasView.exe
            installers/dist/SasView6-macos-latest.dmg
            installers/dist/SasView6-macos-15-intel.dmg
            installers/dist/SasView6.tar.gz
          if-no-files-found: ignore


  test-installer:
    needs: [ build-matrix ]

    strategy:
      matrix:
        os: [ windows-latest, macos-latest, macos-15-intel, ubuntu-22.04 ]
        python-version: [ 3.11 ]
      fail-fast: false

    name: Test installer

    runs-on: ${{ matrix.os }}

    steps:
      - name: Set variables (Windows)
        if: ${{ startsWith(matrix.os, 'windows') }}
        run: |
          echo "INSTALL_PATH=$PWD/sasview/" >> $GITHUB_ENV
          echo "RUN_SASVIEW=$PWD/sasview/sasview.exe" >> $GITHUB_ENV

      - name: Set variables (Linux)
        if: ${{ startsWith(matrix.os, 'ubuntu') }}
        run: |
          echo "INSTALL_PATH=$PWD/sasview/" >> $GITHUB_ENV
          echo "RUN_SASVIEW=$PWD/sasview/sasview" >> $GITHUB_ENV

      - name: Set variables (Windows Powershell)
        if: ${{ startsWith(matrix.os, 'windows') }}
        shell: powershell
        # unfortunately we end up tripping over ourselves with paths
        # later on and need a PS version of the install path
        run: |
          "INSTALL_WIN_PATH=$(Get-Location)/sasview/" >> $env:GITHUB_ENV

      ### Obtain the installer, either from the previous step
      ### or from a previous run of the workflow

      - name: Retrieve installer (workflow image)
        if: ${{ env.INSTALLER_USE_OLD_IMAGE != 'true' }}
        uses: actions/download-artifact@v5
        id: download
        with:
          name: SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }}

      - name: Set variables for download (workflow image)
        if: ${{ env.INSTALLER_USE_OLD_IMAGE != 'true' }}
        run: |
          echo "DL_PATH=${{steps.download.outputs.download-path}}" >> $GITHUB_ENV

      - name: Set variables for download (workflow image) (Windows)
        if: ${{ env.INSTALLER_USE_OLD_IMAGE != 'true' && startsWith(matrix.os, 'windows') }}
        shell: powershell
        run: |
          "DL_WIN_PATH=${{steps.download.outputs.download-path}}" >> $env:GITHUB_ENV

      - name: Retrieve installer (old image)
        if: ${{ env.INSTALLER_USE_OLD_IMAGE == 'true' }}
        id: download-old-installer
        uses: dawidd6/action-download-artifact@v11
        with:
          # Select a specific installer image based on the GitHub Actions run id
          run_id: ${{ env.INSTALLER_OLD_RUNID }}
          name: SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }}
          path: downloads
          if_no_artifact_found: fail

      - name: Set variables for download (old image)
        if: ${{ env.INSTALLER_USE_OLD_IMAGE == 'true' }}
        run: |
          echo "DL_PATH=$PWD/downloads" >> $GITHUB_ENV

      - name: Set variables for download (old image) (Windows)
        if: ${{ env.INSTALLER_USE_OLD_IMAGE == 'true' && startsWith(matrix.os, 'windows') }}
        shell: powershell
        run: |
          "DL_WIN_PATH=$(Get-Location)/downloads" >> $env:GITHUB_ENV

      - name: Check downloads
        run: |
          echo "Sasview downloaded to: $DL_PATH"
          find "$DL_PATH"

      ### Run the installer and check that the installation looks OK

      - name: Run installer (Windows)
        if: ${{ startsWith(matrix.os, 'windows') }}
        # The installer spawns a new process and exits immediately, meaning
        # that the shell thinks it has finishes, tries to exit to move onto
        # the next step in the job, and kills the child process that is
        # trying to do the installation.
        # Wrapping the installer in "Start-Process ... -Wait" means that
        # this step doesn't exit until the installer has finished.
        shell: powershell
        run: |
          Start-Process -FilePath "${{ env.DL_WIN_PATH }}/setupSasView.exe" -ArgumentList "/SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /ALLUSERS /SKIPLICENSE=true /LANG=english /NOICONS /TASKS='' /LOG=${{ env.DL_WIN_PATH }}/install.log /DIR=${{ env.INSTALL_WIN_PATH }}" -Wait

      - name: Check installation log (Windows)
        if: ${{ startsWith(matrix.os, 'windows') }}
        run: |
          echo "Contents of installation log '$DL_PATH/install.log':"
          cat $DL_PATH/install.log

      - name: Run installer (Linux)
        if: ${{ startsWith(matrix.os, 'ubuntu') }}
        run: |
          # $INSTALL_PATH is already the base directory
          tar xf "$DL_PATH/SasView6.tar.gz"

      - name: Check installation files
        if: ${{ !startsWith(matrix.os, 'macos') }}
        run: |
          echo "File listing of SasView installed in '$INSTALL_PATH'"
          find "$INSTALL_PATH"

      - name: Try running the installation (Windows)
        if: ${{ startsWith(matrix.os, 'windows') }}
        # If sasview has crashed on its own, then this should return an error
        run: |
          $RUN_SASVIEW &
          sleep 60s
          kill %1

      - name: Install X11 libraries (Linux)
        if: ${{ startsWith(matrix.os, 'ubuntu') }}
        env:
          DEBIAN_FRONTEND: noninteractive
        run: |
          sudo apt-get -y install libegl1 libopengl0 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils xvfb libxcb-cursor0

      - name: Try running the installation (Linux)
        if: ${{ startsWith(matrix.os, 'ubuntu' )}}
        # If sasview has crashed on its own, then this should return an error
        run: |
          (
            export https_proxy=https://127.0.0.1:9
            echo "## START SASVIEW"
            xvfb-run -a --server-args="-screen 0 1024x768x24" $RUN_SASVIEW 2>&1 |
            tee sasview-test.log
          ) &
          svpid=$!
          (
            set -e
            sleep 20s
            echo "## PULSE CHECK"
            kill -0 $svpid
            echo "## PULSE OK"
            sleep 40s
            echo "## FINISHING SMOKE TEST"
            kill $svpid
            echo "## SMOKE TEST COMPLETE"
            exit 0
          ) &
          pulsepid=$1
          wait $pulsepid
          # Check the log file for successful startup
          grep "SasView session started" sasview-test.log

      - name: Try running the installation (macOS)
        if: ${{ startsWith(matrix.os, 'macOS' )}}
        run: |
          hdiutil attach $DL_PATH/SasView6-${{ matrix.os }}.dmg
          # Testing scripting interface by running simple model generation through sasmodels compare
          /Volumes/SasView6/SasView6.app/Contents/MacOS/sasview -m sasmodels.compare cylinder -noplot

      ### Optionally attempt to work with the debug tarball from pyinstaller

      - name: Retrieve installer debug tarball
        if: ${{ env.INSTALLER_USE_DEBUG == 'true' }}
        id: download-old-debug
        uses: dawidd6/action-download-artifact@v11
        with:
          # Select a specific installer image based on the GitHub Actions run id
          run_id: ${{ env.INSTALLER_OLD_RUNID }}
          name: Debug-SasView-Installer-${{ matrix.os }}-${{ matrix.python-version }}
          path: downloads
          if_no_artifact_found: fail

      - name: Extract debug tarball
        if: ${{ env.INSTALLER_USE_DEBUG == 'true' }}
        run: |
          mkdir unpack
          cd unpack
          tar zxf ../downloads/sasview-pyinstaller-dist.tar.gz

      - name: Check debug installation files
        if: ${{ env.INSTALLER_USE_DEBUG == 'true' && startsWith(matrix.os, 'windows') }}
        run: |
          echo "File listing of SasView install bundle unpacked in 'unpack'"
          find unpack

      - name: Try running debug unpack
        if: ${{ env.INSTALLER_USE_DEBUG == 'true' && startsWith(matrix.os, 'windows') }}
        run: |
          cd unpack/sasview
          ./sasview.exe
