#!/usr/bin/env ruby
# frozen_string_literal: true

require 'fileutils'
require 'pathname'

class Project
  include FileUtils

  attr_reader :name, :title, :weight, :status

  ROOT = Pathname("#{__dir__}/..").expand_path.freeze

  def initialize(name, test_type: :rspec, title: nil, weight:)
    @name = name
    @title = title || name
    @test_type = test_type
    @weight = weight
    @status = :pending
  end

  def self.all
    [
      new('admin', weight: 102),
      new('api', weight: 69),
      new('backend', weight: 282),
      new('backend', test_type: :teaspoon, title: "backend JS", weight: 18),
      new('core', weight: 266),
      new('sample', weight: 28),
      new('legacy_promotions', weight: 63),
      new('promotions', weight: 63)
    ]
  end

  # Return the projects active on the current node
  #
  # @return [Array<Project>]
  def self.weighted_projects(node_total:, node_index:)
    unallocated = all.sort_by(&:weight).reverse
    nodes = Array.new(node_total) { [] }

    while project = unallocated.shift
      nodes.min_by { |projects| projects.sum(&:weight) } << project
    end

    nodes[node_index].tap do |projects|
      warn("Selected #{projects.length} projects(s) on node #{node_index.succ} / #{node_total}")
      projects.each { warn("- #{_1.name}") }
    end
  end

  # Run projects specs
  # @param [Array<Project>] projects
  # @return [Boolean] success of the run
  def self.run(projects = current_projects)
    warn("=" * 80)
    warn("Running projects: #{projects.map(&:name).join(', ')}")

    projects.each do |project|
      warn("=" * 80)
      warn("Building: #{project.name}")
      project.run
    end

    warn("=" * 80)
    warn("Results:")
    projects.each do |project|
      warn("- #{project.name} #{project.status.to_s.upcase}")
    end
    warn("=" * 80)

    projects.all? { _1.status == :success }
  end

  # Run specs for the project
  def run
    @status = :running
    chdir ROOT.join(name) do
      @status = send(:"run_#{@test_type}") ? :success : :failure
    end
  rescue Interrupt
    @status = :interrupted
  rescue StandardError => e
    warn(e)
    @status = :error
  end

  private

  def run_rspec
    run_test_cmd(%W[
      bundle exec rspec
      --profile 10
      --format documentation
      --require rspec_junit_formatter --format RspecJunitFormatter --out #{report_dir}/#{name}.xml
    ])
  end

  def run_teaspoon
    run_test_cmd(%W[
      bundle exec teaspoon
      --require=spec/teaspoon_env.rb
      --format=documentation,junit>#{report_dir}/#{name}_js.xml
    ])
  end

  def run_test_cmd(args)
    puts "Run: #{args.join(' ')}"
    system(*args).tap { puts(_1 ? "Success" : "Failed") }
  end

  def report_dir
    base_dir = Pathname(ENV['CIRCLE_TEST_REPORTS'] || ROOT.join("tmp/test-reports/#{@test_type}"))
    base_dir.join('rspec').tap { mkdir_p _1 }
  end
end

if ENV['CIRCLE_NODE_INDEX'] # Run projects on a CI node
  projects = Project.weighted_projects(
    node_total: Integer(ENV.fetch('CIRCLE_NODE_TOTAL', 1)),
    node_index: Integer(ENV.fetch('CIRCLE_NODE_INDEX', 0)),
  )
else
  projects = Project.all
end

# Run a single project if requested
projects.select! { _1.title == ARGV.first } if ARGV.first

if projects.empty?
  warn("No projects to run")
  exit 0
end

exit Project.run(projects)
