name: Mautic tests and validations

on:
  push:
    branches:
      - '[0-9].*'
  pull_request:
  merge_group:
  schedule:
    # Run every day at 10:45 AM UTC to discover potential issues with dependencies like PHP updates etc.
    - cron: '45 10 * * *'

permissions:
  contents: read # to fetch code (actions/checkout)

jobs:
  phpunit:
    # We don't want the scheduled jobs to run on forks of Mautic
    if: (github.event_name == 'schedule' && (github.repository == 'mautic/mautic' || github.repository == 'mautic/api-library') ) || (github.event_name != 'schedule')
    runs-on: ubuntu-latest

    strategy:
      matrix:
        php-versions: ['8.2', '8.3', '8.4']
        db-types: ['mysql:8.4', 'mysql:9.2', 'mariadb:10.11', 'mariadb:11.4']

    name: PHPUnit ${{ matrix.php-versions }} ${{ matrix.db-types }}

    services:
      database:
        image: ${{ matrix.db-types }}
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_DATABASE: mautictest
        ports:
          - 3306
        options: >-
          --shm-size=2gb
          --tmpfs=/var/lib/mysql
          --health-cmd "${{ startsWith(matrix.db-types, 'mysql:') && 'mysqladmin ping' || 'healthcheck.sh --connect --innodb_initialized' }}"
          --health-interval=10s 
          --health-timeout=5s 
          --health-retries=3
      saml:
        image: kenchan0130/simplesamlphp
        env:
          SIMPLESAMLPHP_SP_ENTITY_ID: https://localhost
          SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: https://localhost/s/saml/login_check
          SIMPLESAMLPHP_IDP_BASE_URL: http://localhost:8086/simplesaml/
        ports:
          - 8086:8080

    steps:
    - uses: actions/checkout@v4
      # Codecov needs access to previous commits, so we add fetch-depth: 0
      with:
        fetch-depth: 0

    - name: PHPUnit cache
      if: ${{matrix.php-versions == '8.2' && matrix.db-types == 'mariadb:10.11'}}
      uses: actions/cache@v4
      with:
        path: ./var/cache/phpunit
        key: ${{ runner.os }}-phpunit-${{ hashFiles('**/composer.lock') }}-${{ github.run_id }}
        restore-keys: |
          ${{ runner.os }}-phpunit-${{ hashFiles('**/composer.lock') }}
          ${{ runner.os }}-phpunit-

    - name: Setup PHP, with composer and extensions
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php-versions }}
        extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, pdo_mysql
        coverage: pcov
        ini-values: pcov.enabled=0, pcov.directory=., pcov.exclude="~tests|themes|vendor~", zend.assertions=1

    - name: add MySQL config file
      run: |
        mysqldump --version
        mysqldump --print-defaults
        cp .github/ci-files/.my.cnf ~/.my.cnf
        mysqldump --print-defaults
        DB_LOG_NAME="${{ matrix.db-types }}"
        echo "DB_LOG_NAME=${DB_LOG_NAME/:/_}" >> $GITHUB_ENV

    - name: Set SYMFONY_ENV to test
      run: |
        echo "SYMFONY_ENV=test" >> $GITHUB_ENV
        echo "MAUTIC_ENV=test" >> $GITHUB_ENV

    - name: Install dependencies
      # composer install cache - https://github.com/ramsey/composer-install
      uses: "ramsey/composer-install@v3"

    - name: Run tests - database = ${{ matrix.db-types }}
      run: |
        rm -rf var/cache
        export DB_PORT="${{ job.services.database.ports[3306] }}"
        export SAML_PORT="${{ job.services.saml.ports[8080] }}"

        if [[ "${{ matrix.php-versions }}" == "8.2" ]] && [[ "${{ matrix.db-types }}" == "mariadb:10.11" ]]; then
          php -d zend.assertions=1 -d pcov.enabled=1 bin/phpunit -d memory_limit=3G --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist --coverage-clover=coverage.xml --log-junit=junit.xml
        else
          php -d zend.assertions=1 bin/phpunit -d memory_limit=2G --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist
        fi

    - name: Upload coverage report
      if: ${{ matrix.php-versions == '8.2' && matrix.db-types == 'mariadb:10.11' && github.repository == 'mautic/mautic' }}
      uses: codecov/codecov-action@v5
      with:
        files: ./coverage.xml
        fail_ci_if_error: true
      env:
        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

    - name: Save test results for future steps
      if: ${{ matrix.php-versions == '8.2' && matrix.db-types == 'mariadb:10.11' && github.repository == 'mautic/mautic' }}
      uses: actions/upload-artifact@v4
      with:
        name: test-results
        path: |
          ./junit.xml
          ./coverage.xml

    - name: Slack Notification if tests fail
      uses: rtCamp/action-slack-notify@v2
      if: ${{ failure() && github.event_name == 'schedule' }}
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_MESSAGE: 'The daily GitHub Actions tests in mautic/mautic have failed. Most likely something external has changed, like a PHP version update.'

    - name: Store log artifacts
      if: ${{ always() }}
      uses: actions/upload-artifact@v4
      with:
        name: logs-${{ matrix.php-versions }}-${{ env.DB_LOG_NAME }}
        path: ./var/logs/*

  e2e-tests:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Setup DDEV
      uses: ddev/github-action-setup-ddev@v1

    - name: Set up environment variables for test mode
      run: echo -e "APP_ENV=test\nAPP_DEBUG=1" > .env.local

    - name: Run E2E tests
      run: ddev composer e2e-test

    - name: Upload test artifacts
      if: failure()
      uses: actions/upload-artifact@v4
      with:
        name: e2e-test-screenshots
        path: tests/_output
        if-no-files-found: ignore

  misc:
    # We don't want the scheduled jobs to run on forks of Mautic
    if: (github.event_name == 'schedule' && (github.repository == 'mautic/mautic' || github.repository == 'mautic/api-library') ) || (github.event_name != 'schedule')
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        commands: ['PHPSTAN', 'CS Fixer', 'Rector', 'Rector tests', 'Twig Lint', 'scaffolded files mismatch', 'PHPStan baseline changes', 'composer install', 'composer lock check']
        php-versions: ['8.2']

    name: ${{ matrix.commands }} - ${{ matrix.php-versions }}

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

    - name: Rector Cache
      if: ${{matrix.commands == 'Rector'}}
      uses: actions/cache@v4
      with:
        path: ./var/cache/rector
        key: ${{ runner.os }}-rector-${{ hashFiles('**/composer.lock') }}-${{ github.run_id }}
        restore-keys: |
          ${{ runner.os }}-rector-${{ hashFiles('**/composer.lock') }}
          ${{ runner.os }}-rector-

    - name: Rector tests Cache
      if: ${{matrix.commands == 'Rector tests'}}
      uses: actions/cache@v4
      with:
        path: ./var/cache/rector-tests
        key: ${{ runner.os }}-rector-tests-${{ hashFiles('**/composer.lock') }}-${{ github.run_id }}
        restore-keys: |
          ${{ runner.os }}-rector-tests-${{ hashFiles('**/composer.lock') }}
          ${{ runner.os }}-rector-tests-

    - name: PHPStan Cache
      if: ${{matrix.commands == 'PHPSTAN'}}
      uses: actions/cache@v4
      with:
        path: ./var/phpstan-cache
        key: ${{ runner.os }}-phpstan-${{ hashFiles('**/composer.lock') }}-${{ github.run_id }}
        restore-keys: |
          ${{ runner.os }}-phpstan-${{ hashFiles('**/composer.lock') }}
          ${{ runner.os }}-phpstan-

    - name: NPM Cache
      uses: actions/cache@v4
      with:
        path: ./var/cache/js
        key: ${{ runner.os }}-js-${{ hashFiles('**/package-lock.json', 'webpack.config.js') }}-${{ github.run_id }}
        restore-keys: |
          ${{ runner.os }}-js-${{ hashFiles('**/package-lock.json', 'webpack.config.js') }}
          ${{ runner.os }}-js-

    - name: Get changed files
      id: changed-files
      uses: tj-actions/changed-files@v46

    - name: Setup PHP, with composer and extensions
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php-versions }}
        extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, pdo_mysql

    - name: Install dependencies
      # composer install cache - https://github.com/ramsey/composer-install
      uses: "ramsey/composer-install@v3"
      env:
        NPM_CONFIG_CACHE: ./var/cache/js/npm

    - name: Run ${{ matrix.commands }}
      run: |
        if [[ "${{ matrix.commands }}" == "PHPSTAN" ]]; then
          export SYMFONY_ENV=dev
          export APP_ENV=dev
          export APP_DEBUG=1
          composer phpstan -- --no-progress
        elif [[ "${{ matrix.commands }}" == "Rector" ]]; then
          export SYMFONY_ENV=test
          bin/console cache:warmup
          bin/rector --dry-run --ansi
        elif [[ "${{ matrix.commands }}" == "Rector tests" ]]; then
          export SYMFONY_ENV=test
          bin/console cache:warmup
          bin/rector --config=rector-tests.php --dry-run --ansi
        elif [[ "${{ matrix.commands }}" == "Twig Lint" ]]; then
          bin/console lint:twig app plugins
        elif [[ "${{ matrix.commands }}" == "CS Fixer" ]]; then
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            if [[ $file == *.php ]]; then
              cs_fix_files="${cs_fix_files} $file"
            fi
          done
          if [[ $cs_fix_files ]]; then
            bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --using-cache=no --show-progress=dots --diff $cs_fix_files
          fi
        elif [[ "${{ matrix.commands }}" == "scaffolded files mismatch" ]]; then
          wget -q -O /tmp/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 && chmod 755 /tmp/jq
          /tmp/jq -r '.extra["mautic-scaffold"]["file-mapping"] | to_entries[] | "diff -q \(.key | sub("\\[(project|web)-root\\]";".")) app/\(.value)"' app/composer.json > diff_commands.sh
          /tmp/jq -r '.extra["mautic-scaffold"]["file-mapping"] | to_entries[] | "diff -rui \(.key | sub("\\[(project|web)-root\\]";".")) app/\(.value)"' app/composer.json > diff_commands_verbose.sh
          bash diff_commands.sh 2>&1 | tee /tmp/diff_command_output.txt
          rm diff_commands.sh
          if [[ $(wc -l </tmp/diff_command_output.txt) -ge 1 ]]; then
            echo "some scaffolded files were not updated or deleted"
            echo "Please apply the same changes in the files mentioned above"
            echo "verbose diff:"
            bash diff_commands_verbose.sh
            exit 1
          fi
        elif [[ "${{ matrix.commands }}" == "PHPStan baseline changes" ]]; then
          if [[ "${{ steps.changed-files.outputs.modified_files }}" == *"phpstan-baseline.neon"* ]]; then
            stat=$(git diff --shortstat "origin/${{ github.base_ref }}" ${{ github.sha }} -- phpstan-baseline.neon)
            echo $stat
            regex="[0-9]+[[:space:]]insertion"
            if [[ $stat =~ $regex ]]; then
              echo "There are modifications (added or changed lines) to the phpstan-baseline.neon"
              echo "Please fix the PHPStan errors instead of altering the baseline file"
              exit 1
            fi
          fi
        elif [[ "${{ matrix.commands }}" == "composer install" ]]; then
          # create a temp dir and mimic a composer install via mautic/recommended-project
          mkdir test_composer
          cd test_composer
          cp ../.github/ci-files/composer.json ./
          composer install

          # test if media/css and media/js folder contain the same files as the tarball releases
          test -z "$(comm -23 <(ls ../media/js  | sort) <(ls docroot/media/js  | sort))"
          test -z "$(comm -23 <(ls ../media/css | sort) <(ls docroot/media/css | sort))"

          # test if console is executable
          ./bin/console cache:clear
        elif [[ "${{ matrix.commands }}" == "composer lock check" ]]; then
          composer validate --ansi
        else
          echo "Invalid command"
          exit 1
        fi

    - name: Slack Notification if tests fail
      uses: rtCamp/action-slack-notify@v2
      if: ${{ failure() && github.event_name == 'schedule' }}
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_MESSAGE: 'The daily GitHub Actions tests in mautic/mautic have failed. Most likely something external has changed, like a PHP version update.'
