require 'kraken-mobile/helpers/feature_analyzer'
require 'kraken-mobile/mobile/mobile_process'
require 'kraken-mobile/models/feature_file'
require 'kraken-mobile/models/web_device'
require 'kraken-mobile/web/web_process'
require 'kraken-mobile/utils/reporter'
require 'kraken-mobile/mobile/adb'
require 'kraken-mobile/utils/k.rb'
require 'parallel'

class TestScenario
  #-------------------------------
  # Fields
  #-------------------------------
  attr_accessor :devices
  attr_accessor :kraken_app
  attr_accessor :feature_file
  attr_accessor :execution_id
  attr_accessor :reporter

  #-------------------------------
  # Constructors
  #-------------------------------
  def initialize(kraken_app:, feature_file_path:)
    @feature_file = FeatureFile.new(file_path: feature_file_path)
    @devices = sample_devices
    @kraken_app = kraken_app
    @execution_id = Digest::SHA256.hexdigest(Time.now.to_f.to_s)
    @reporter = Reporter.new(test_scenario: self)

    ensure_apk_specified_if_necessary
  end

  #-------------------------------
  # Lifecycle
  #-------------------------------
  def before_execution
    setup_scenario_environment_variables
    delete_all_web_inboxes
    File.delete(K::DIRECTORY_PATH) if File.exist?(K::DIRECTORY_PATH)
    File.delete(K::DICTIONARY_PATH) if File.exist?(K::DICTIONARY_PATH)
    K::PROCESS_STATE_FILE_PATH.each do |_state, file_path|
      File.delete(file_path) if File.exist?(file_path)
    end
    @reporter.create_report_folder_requirements
  end

  def run
    unless @feature_file.right_syntax?
      raise "ERROR: Verify feature file #{@file_path} has one unique @user tag"\
      ' for each scenario'
    end

    before_execution
    execute
    after_execution
  end

  def after_execution
    File.delete(K::DIRECTORY_PATH) if File.exist?(K::DIRECTORY_PATH)
    File.delete(K::DICTIONARY_PATH) if File.exist?(K::DICTIONARY_PATH)
    K::PROCESS_STATE_FILE_PATH.each do |_state, file_path|
      File.delete(file_path) if File.exist?(file_path)
    end
    @reporter.save_report
    notify_scenario_finished
  end

  #-------------------------------
  # Methods
  #-------------------------------
  def execute
    Parallel.map_with_index(
      @devices, in_threads: @devices.count
    ) do |device, index|
      unless device.nil?
        user_id = index + 1
        start_process_for_user_id_in_device(
          user_id, device
        )
      end
    end
  end

  def self.ready_to_start?
    process_ids = DeviceProcess.registered_process_ids
    processes_ready = DeviceProcess.processes_in_state(
      K::PROCESS_STATES[:ready_to_start]
    )
    process_ids.all? do |process_id|
      processes_ready.include?(process_id) ||
        Device.find_by_process_id(process_id)&.connected? == false
    end
  end

  def self.ready_to_finish?
    process_ids = DeviceProcess.registered_process_ids
    processes_finished = DeviceProcess.processes_in_state(
      K::PROCESS_STATES[:ready_to_finish]
    )
    process_ids.all? do |process_id|
      processes_finished.include?(process_id) ||
        Device.find_by_process_id(process_id)&.connected? == false
    end
  end

  def requires_predefined_devices?
    !ENV[K::CONFIG_PATH].nil?
  end

  private

  def start_process_for_user_id_in_device(user_id, device)
    if device.is_a? AndroidDevice
      start_mobile_process_for_user_id_in_device(user_id, device)
    elsif device.is_a? WebDevice
      start_web_process_for_user_id_in_device(user_id, device)
    else
      raise 'ERROR: Platform not supported'
    end
  end

  def start_mobile_process_for_user_id_in_device(user_id, device)
    MobileProcess.new(
      id: user_id,
      device: device,
      test_scenario: self
    ).run
  end

  def start_web_process_for_user_id_in_device(user_id, device)
    WebProcess.new(
      id: user_id,
      device: device,
      test_scenario: self
    ).run
  end

  def sample_devices
    return predefined_devices if requires_predefined_devices?

    mobile = sample_mobile_devices
    web = sample_web_devices
    @feature_file.sorted_required_devices.map do |device|
      device[:system_type] == '@web' ? web.shift : mobile.shift
    end
  end

  def sample_mobile_devices
    android_devices = ADB.connected_devices
    android_devices.sample(
      @feature_file.number_of_required_mobile_devices
    )
  end

  def sample_web_devices
    web_devices = []
    @feature_file.number_of_required_web_devices.times do
      web_devices << WebDevice.factory_create
    end
    web_devices
  end

  def notify_scenario_finished
    @kraken_app.on_test_scenario_finished
  end

  def user_id_is_mobile?(user_id)
    complement_tags = @feature_file.tags_for_user_id(user_id).map(
      &:downcase
    )
    complement_tags.include?('@mobile')
  end

  def user_id_is_web?(user_id)
    complement_tags = @feature_file.tags_for_user_id(user_id).map(
      &:downcase
    )
    complement_tags.include?('@web')
  end

  def delete_all_web_inboxes
    Dir.glob(".*_#{K::INBOX_FILE_NAME}").each do |file|
      File.delete(file)
    end
  end

  def predefined_devices
    config_absolute_path = File.expand_path(ENV[K::CONFIG_PATH])
    file = open(config_absolute_path)
    content = file.read
    file.close
    devices_json = JSON.parse(content).values
    devices_json.map do |device_json|
      if device_json['type'] == K::ANDROID_DEVICE
        AndroidDevice.new(
          id: device_json['id'], model: device_json['model']
        )
      elsif device_json['type'] == K::WEB_DEVICE
        WebDevice.new(
          id: device_json['id'], model: device_json['model']
        )
      else
        raise 'ERROR: Platform not supported'
      end
    end
  end

  def apk_required?
    sample_mobile_devices.any? && ENV[K::CONFIG_PATH].nil?
  end

  def ensure_apk_specified_if_necessary
    return unless apk_required?
    return unless @kraken_app&.apk_path.nil?

    raise 'ERROR: Invalid APK file path'
  end

  def setup_scenario_environment_variables
    return if @kraken_app.nil? || @reporter.nil?

    @kraken_app.save_value_in_environment_variable_with_name(
      name: K::SCREENSHOT_PATH, value: @reporter.screenshot_path
    )
  end
end