name: CI

on:
  pull_request:
  workflow_dispatch:
    inputs:
      casks:
        description: List of casks to audit (comma-separated)
        required: true
      skip_install:
        description: Skip installation of casks
        required: false
        default: true
        type: boolean
      new_cask:
        description: Apply new cask audit
        required: false
        default: false
        type: boolean

env:
  HOMEBREW_DEVELOPER: 1
  HOMEBREW_NO_AUTO_UPDATE: 1
  HOMEBREW_NO_INSTALL_FROM_API: 1
  HOMEBREW_GITHUB_API_TOKEN: ${{ github.token }}

concurrency:
  group: "${{ github.ref }}"
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
  contents: read

jobs:
  generate-matrix:
    outputs:
      matrix: ${{ steps.generate-matrix.outputs.matrix }}
    runs-on: macos-latest
    steps:
      - name: Set up Homebrew
        id: set-up-homebrew
        uses: Homebrew/actions/setup-homebrew@master
        with:
          core: false
          cask: true
          test-bot: false

      - name: Check out Pull Request
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Generate CI matrix
        id: generate-matrix
        run: |
          if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]
          then
            brew ruby -- "$(brew --repository homebrew/cask)/cmd/lib/generate-matrix.rb" ${{ github.event.inputs.skip_install  && '--skip-install' }} ${{ github.event.inputs.new_cask  && '--new' }} --casks=${{ github.event.inputs.casks }}
          else
            brew ruby -- "$(brew --repository homebrew/cask)/cmd/lib/generate-matrix.rb" --url="${{ github.event.pull_request.url }}"
          fi

  test:
    name: ${{ matrix.name }}
    needs: generate-matrix
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
    steps:
      - name: Set up Homebrew
        id: set-up-homebrew
        uses: Homebrew/actions/setup-homebrew@master
        with:
          core: false
          cask: true
          test-bot: false

      - name: Enable debug mode
        run: |
          echo 'HOMEBREW_DEBUG=1' >> "${GITHUB_ENV}"
          echo 'HOMEBREW_VERBOSE=1' >> "${GITHUB_ENV}"
        if: runner.debug

      - name: Check out Pull Request
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Clean up CI machine
        run: |
          if ! brew uninstall --cask julia && ! rm -r /Applications/Julia-*.app; then
            echo '::warning::Removing Julia is no longer necessary.'
          fi

          if ! rm /usr/local/bin/dotnet; then
            echo '::warning::Removing `dotnet` symlink is no longer necessary.'
          fi

          if ! rm /usr/local/bin/pod; then
            echo '::warning::Removing `cocoapods` symlink is no longer necessary.'
          fi

          if ! rm /usr/local/bin/chromedriver; then
            echo '::warning::Removing `chromedriver` symlink is no longer necessary.'
          fi

          brew unlink python && brew link --overwrite python
        if: runner.os == 'macOS'

      - name: Cache Homebrew Gems
        id: cache
        uses: actions/cache@v4
        with:
          path: ${{ steps.set-up-homebrew.outputs.gems-path }}
          key: ${{ matrix.runner }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
          restore-keys: ${{ matrix.runner }}-rubygems-

      - name: Install Homebrew Gems
        id: gems
        run: brew install-bundler-gems
        if: steps.cache.outputs.cache-hit != 'true'

      - name: Run brew readall ${{ matrix.tap }}
        id: readall
        run: brew readall --os=all --arch=all '${{ matrix.tap }}'
        if: >
          always() &&
          contains(fromJSON('["success", "skipped"]'), steps.gems.outcome) &&
          !matrix.skip_readall

      - name: Run brew style ${{ matrix.tap }}
        run: brew style '${{ matrix.tap }}'
        if: >
          always() &&
          contains(fromJSON('["success", "skipped"]'), steps.readall.outcome) &&
          !matrix.cask

      - name: Run brew fetch --cask ${{ matrix.cask.token }}
        id: fetch
        run: |
          brew fetch --cask --retry --force ${{ join(matrix.fetch_args, ' ') }} '${{ matrix.cask.path }}'
        timeout-minutes: 30
        if: >
          always() &&
          contains(fromJSON('["success", "skipped"]'), steps.readall.outcome) &&
          matrix.cask

      - name: Run brew audit --cask${{ (matrix.cask && ' ') || ' --tap ' }}${{ matrix.cask.token || matrix.tap }}
        id: audit
        run: |
          brew audit --cask ${{ join(matrix.audit_args, ' ') }}${{ (matrix.cask && ' ') || ' --tap ' }}'${{ matrix.cask.token || matrix.tap }}'
        timeout-minutes: 30
        if: >
          always() &&
          contains(fromJSON('["success", "skipped"]'), steps.readall.outcome) &&
          (!matrix.cask || steps.fetch.outcome == 'success') &&
          !matrix.skip_audit

      - name: Gather cask information
        id: info
        run: |
          brew tap homebrew/cask-versions
          brew ruby <<'EOF'
            require 'cask/cask_loader'
            require 'cask/installer'

            cask = Cask::CaskLoader.load('${{ matrix.cask.path }}')

            was_installed = cask.installed?
            manual_installer = cask.artifacts.any? { |artifact|
              artifact.is_a?(Cask::Artifact::Installer::ManualInstaller)
            }

            macos_requirement_satisfied = if macos_requirement = cask.depends_on.macos
              macos_requirement.satisfied?
            else
              true
            end

            cask_conflicts = cask.conflicts_with&.dig(:cask).to_a.select { |c| Cask::CaskLoader.load(c).installed? }
            formula_conflicts = cask.conflicts_with&.dig(:formula).to_a.select { |f| Formula[f].any_version_installed? }

            installer = Cask::Installer.new(cask)
            cask_and_formula_dependencies = installer.missing_cask_and_formula_dependencies

            cask_dependencies = cask_and_formula_dependencies.select { |d| d.is_a?(Cask::Cask) }.map(&:full_name)
            formula_dependencies = cask_and_formula_dependencies.select { |d| d.is_a?(Formula) }.map(&:full_name)

            File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f|
              f.puts "was_installed=#{JSON.generate(was_installed)}"
              f.puts "manual_installer=#{JSON.generate(manual_installer)}"
              f.puts "macos_requirement_satisfied=#{JSON.generate(macos_requirement_satisfied)}"
              f.puts "cask_conflicts=#{JSON.generate(cask_conflicts)}"
              f.puts "cask_dependencies=#{JSON.generate(cask_dependencies)}"
              f.puts "formula_conflicts=#{JSON.generate(formula_conflicts)}"
              f.puts "formula_dependencies=#{JSON.generate(formula_dependencies)}"
            end
          EOF
        if: always() && steps.fetch.outcome == 'success' && matrix.cask

      - name: Uninstall conflicting formulae
        run: |
          brew uninstall --formula ${{ join(fromJSON(steps.info.outputs.formula_conflicts), ' ') }}
        if: always() && steps.info.outcome == 'success' && join(fromJSON(steps.info.outputs.formula_conflicts)) != ''
        timeout-minutes: 30

      - name: Uninstall conflicting casks
        run: |
          brew uninstall --cask ${{ join(fromJSON(steps.info.outputs.cask_conflicts), ' ') }}
        if: always() && steps.info.outcome == 'success' && join(fromJSON(steps.info.outputs.cask_conflicts)) != ''
        timeout-minutes: 30

      - name: Run brew uninstall --cask --zap ${{ matrix.cask.token }}
        run: |
          brew uninstall --cask --zap '${{ matrix.cask.path }}'
        if: always() && steps.info.outcome == 'success' && fromJSON(steps.info.outputs.was_installed)
        timeout-minutes: 30

      - name: Take snapshot of installed and running apps and services
        id: snapshot
        run: |
          brew ruby -r "$(brew --repository homebrew/cask)/cmd/lib/check.rb" <<'EOF'
            File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f|
              f.puts "before=#{JSON.generate(Check.all)}"
            end
          EOF
        if: always() && steps.info.outcome == 'success'

      - name: Run brew install --cask ${{ matrix.cask.token }}
        id: install
        run: brew install --cask '${{ matrix.cask.path }}'
        if: >
          always() && steps.info.outcome == 'success' &&
          fromJSON(steps.info.outputs.macos_requirement_satisfied) &&
          !matrix.skip_install
        timeout-minutes: 30

      - name: Run brew uninstall --cask ${{ matrix.cask.token }}
        run: brew uninstall --cask '${{ matrix.cask.path }}'
        if: always() && steps.install.outcome == 'success' && !fromJSON(steps.info.outputs.manual_installer)
        timeout-minutes: 30

      - name: Uninstall formula dependencies
        run: |
          brew uninstall --formula ${{ join(fromJSON(steps.info.outputs.formula_dependencies), ' ') }}
        if: always() && steps.install.outcome == 'success' && join(fromJSON(steps.info.outputs.formula_dependencies)) != ''
        timeout-minutes: 30

      - name: Uninstall cask dependencies
        run: |
          brew uninstall --cask ${{ join(fromJSON(steps.info.outputs.cask_dependencies), ' ') }}
        if: always() && steps.install.outcome == 'success' && join(fromJSON(steps.info.outputs.cask_dependencies)) != ''
        timeout-minutes: 30

      - name: Compare installed and running apps and services with snapshot
        run: |
          brew ruby -r "$(brew --repository homebrew/cask)/cmd/lib/check.rb" <<'EOF'
            require "cask/cask_loader"
            require "utils/github/actions"

            before = JSON.parse(<<~'EOS').transform_keys(&:to_sym)
              ${{ steps.snapshot.outputs.before }}
            EOS
            after = Check.all

            cask = Cask::CaskLoader.load('${{ matrix.cask.path }}')
            errors = Check.errors(before, after, cask: cask)

            errors.each do |error|
              onoe error
              puts GitHub::Actions::Annotation.new(:error, error, file: '${{ matrix.cask.path }}')
            end

            exit 1 if errors.any?
          EOF
        if: always() && steps.snapshot.outcome == 'success'

  conclusion:
    name: conclusion
    needs: test
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Result
        run: ${{ needs.test.result == 'success' }}
