# frozen_string_literal: true

class TestrunMessage < ApplicationRecord
  belongs_to :testrun

  enum cmd: {
    input: 0,
    write: 1,
    clear: 2,
    turtle: 3,
    turtlebatch: 4,
    render: 5,
    exit: 6,
    status: 7,
    hint: 8,
    client_kill: 9,
    exception: 10,
    result: 11,
    canvasevent: 12,
    files: 13,
  }, _default: :write, _prefix: true

  enum stream: {
    stdin: 0,
    stdout: 1,
    stderr: 2,
  }, _prefix: true

  validates :cmd, presence: true
  validates :timestamp, presence: true
  validates :stream, length: {minimum: 0, allow_nil: false}, if: -> { cmd_write? }
  validates :log, length: {minimum: 0, allow_nil: false}, if: -> { cmd_write? }
  validate :either_data_or_log

  default_scope { order(timestamp: :asc) }
  scope :output, -> { where(cmd: 1, stream: %i[stdout stderr]) }

  def self.create_for(testrun, messages)
    # We don't want to store anything if the testrun passed
    return if testrun.passed?

    messages.map! do |message|
      # We create a new hash and move all known keys
      result = {}
      result[:testrun] = testrun
      result[:log] = (message.delete(:log) || message.delete(:data)) if message[:cmd] == :write || message.key?(:log)
      result[:timestamp] = message.delete :timestamp
      result[:stream] = message.delete :stream if message.key?(:stream)
      result[:cmd] = message.delete :cmd
      # The remaining keys will be stored in the `data` column
      result[:data] = message.presence if message.present?
      result
    end

    # Before storing all messages, we truncate some to save storage
    filtered_messages = filter_messages_by_size testrun, messages

    # An array with hashes is passed, all are stored
    validate_and_store!(filtered_messages)
  end

  def self.filter_messages_by_size(testrun, messages)
    limits = if testrun.submission.cause == 'requestComments'
               {data: {limit: 25, size: 0}, log: {limit: 5000, size: 0}}
             else
               {data: {limit: 10, size: 0}, log: {limit: 500, size: 0}}
             end

    filtered_messages = messages.map do |message|
      if message.key?(:log) && limits[:log][:size] < limits[:log][:limit]
        message[:log] = message[:log][0, limits[:log][:limit] - limits[:log][:size]]
        limits[:log][:size] += message[:log].size
      elsif message[:data] && limits[:data][:size] < limits[:data][:limit]
        limits[:data][:size] += 1
      elsif !message.key?(:log) && limits[:data][:size] < limits[:data][:limit]
        # Accept short TestrunMessages (e.g. just transporting a status information)
        # without increasing the `limits[:data][:limit]` before the limit is reached
      else
        # Clear all remaining messages
        message = nil
      end
      message
    end
    filtered_messages.select(&:present?)
  end
  private_class_method :filter_messages_by_size

  def self.validate_and_store!(messages)
    validated_messages = messages.map do |message|
      testrun_message = TestrunMessage.new(message)
      testrun_message.validate!
      # We serialize the message without the ID, created_at and updated_at, as they are generated by the database.
      testrun_message.serializable_hash(except: %w[id created_at updated_at])
    end

    # Now, we store all messages and skip validations (they are already done)
    TestrunMessage.insert_all!(validated_messages) if validated_messages.present? # rubocop:disable Rails/SkipsModelValidations
  end
  private_class_method :validate_and_store!

  def either_data_or_log
    if [data, log].count(&:present?) > 1
      errors.add(log, "can't be present if data is also present")
    end
  end
  private :either_data_or_log
end
