#!/usr/bin/env ruby
# Copyright (c) 2021-2022 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# frozen_string_literal: true

# Huawei Technologies Co.,Ltd.

# Class represents full specification with additional information needed for
# coverage computation
class Spec
  attr_reader :data, :orphaned

  def initialize(arr)
    # Full spec object
    @data = merge_specs(arr)

    # Array of tests not matching the spec
    @orphaned = []

    prepare_spec
  end

  def load_non_testable(nt_data)
    nt_data['groups']&.each do |ntg|
      spec_group = @data['groups'].find { |sg| sg['title'] == ntg['title'] }
      if spec_group.nil?
        warn "Non testable group \"#{ntg['title']}\" not found in ISA."
      else
        process_non_testable_group(spec_group, ntg)
      end
    end
  end

  def load_tests(testdir, testglob)
    Dir.glob(testglob, base: testdir) do |f|
      process_test_file(testdir, f)
    rescue StandardError => e
      @orphaned << { 'file' => f, 'error' => e, 'comment' => e.backtrace }
    end
  end

  private

  def merge_specs(sources)
    target = sources[0]
    sources.drop(1).each { |f| merge_spec(target, f) }
    target
  end

  def merge_spec(target, src)
    %w[prefixes groups properties exceptions verification version min_version chapters].each do |attr|
      if target[attr]
        target[attr] += src[attr] if src[attr]
      else
        target[attr] = src[attr]
      end
    end
  end

  def prepare_spec
    @data['groups'].each do |g|
      prepare_spec_group(g)
    end
  end

  def prepare_spec_group(grp)
    prepare_description(grp)
    prepare_instructions(grp)
    prepare_exceptions(grp)
    prepare_verifications(grp)
  end

  def prepare_instructions(grp)
    grp['instructions'].each do |i|
      i['tests'] = []
      i['non_testable'] = false
    end
  end

  def prepare_description(grp)
    grp['description_tests'] = split(grp['description']).map do |da|
      { 'assertion' => da, 'tests' => [], 'non_testable' => false }
    end
  end

  def prepare_exceptions(grp)
    grp['exceptions_tests'] = grp['exceptions'].map do |e|
      { 'exception' => e, 'tests' => [], 'non_testable' => false }
    end
  end

  def prepare_verifications(grp)
    grp['verification_tests'] = grp['verification'].map do |v|
      { 'verification' => v, 'tests' => [], 'non_testable' => false }
    end
  end

  def process_non_testable_group(spec_group, ntg)
    process_non_testable_description(spec_group, ntg)
    process_non_testable_instructions(spec_group, ntg)
    process_non_testable_exceptions(spec_group, ntg)
    process_non_testable_verifications(spec_group, ntg)
  end

  def process_non_testable_description(spec_group, ntg)
    ntg['description'] && split(ntg['description']).each do |ntda|
      spec_description = spec_group['description_tests'].find { |sd| same?(sd['assertion'], ntda) }
      if spec_description.nil?
        warn "Non testable description \"#{ntda}\" in group \"#{ntg['title']}\" not found in iSA."
      else
        spec_description['non_testable'] = true
      end
    end
  end

  def process_non_testable_instructions(spec_group, ntg)
    ntg['instructions']&.each do |nti|
      spec_instruction = spec_group['instructions'].find { |si| si['sig'] == nti['sig'] }
      if spec_instruction.nil?
        warn "Non testable instruction \"#{nti['sig']}\" in group \"#{ntg['title']}\" not found in ISA."
      else
        spec_instruction['non_testable'] = true
      end
    end
  end

  def process_non_testable_exceptions(spec_group, ntg)
    ntg['exceptions']&.each do |nte|
      spec_exception = spec_group['exceptions_tests'].find { |se| se['exception'] == nte }
      if spec_exception.nil?
        warn "Non testable exception \"#{nte}\" in group \"#{ntg['title']}\" not found in ISA."
      else
        spec_exception['non_testable'] = true
      end
    end
  end

  def process_non_testable_verifications(spec_group, ntg)
    ntg['verification']&.each do |ntv|
      spec_verification = spec_group['verification_tests'].find { |sv| sv['verification'] == ntv }
      if spec_verification.nil?
        warn "Non testable verification \"#{ntv}\" in group \"#{ntg['title']}\" not found in ISA."
      else
        spec_verification['non_testable'] = true
      end
    end
  end

  # split long-text description into array of assertions
  def split(description) # rubocop:disable Metrics
    result = []
    small = false
    description.split(/\./).each do |p|
      if small
        result[-1] += ".#{p}"
        small = false if p.length > 5
      elsif p.length > 5
        result << p.lstrip
      else
        if result.length.zero?
          result << p.lstrip
        else
          result[-1] += ".#{p}"
        end
        small = true
      end
    end
    result
  end

  def same?(str1, str2)
    str1.tr('^A-Za-z0-9', '').downcase == str2.tr('^A-Za-z0-9', '').downcase
  end

  def process_test_file(testdir, file)
    raw = read_test_data(File.join(testdir, file))
    tdata = YAML.safe_load(raw)

    if tdata.class != Array || tdata.length.zero?
      @orphaned << { 'file' => file, 'error' => 'Bad test format, expected array of titles', 'comment' => raw }
      return
    end

    tdata.each do |tg|
      process_test_data(tg, file)
    end
  end

  def read_test_data(filename)
    lines_array = []
    started = false
    File.readlines(filename).each do |line|
      started = true if line[0..3] == '#---'
      lines_array << line[1..-1] if started
      break if line[0] != '#'
    end
    lines_array.join("\n")
  end

  def process_test_data(test_group, file)
    spec_group = @data['groups'].find { |g| g['title'] == test_group['title'] }
    if spec_group.nil?
      @orphaned << { 'file' => file, 'error' => 'Group with given title not found in the ISA', 'comment' => test_group }
      return
    end

    assertions = proc_test_instructions(test_group, spec_group, file) +
                 proc_test_descriptions(test_group, spec_group, file) +
                 proc_test_exceptions(test_group, spec_group, file) +
                 proc_test_verifications(test_group, spec_group, file)
    if assertions.zero?
      @orphaned << { 'file' => file, 'error' => 'Test header doesn\'t match any assertions in ISA',
                     'comment' => test_group }
    end
  end

  def proc_test_instructions(test_group, spec_group, file)
    cnt = 0
    test_group['instructions']&.each do |ti|
      cnt += proc_test_instruction(ti, spec_group, file)
    end
    cnt
  end

  def proc_test_instruction(test_instr, spec_group, file)
    gi = spec_group['instructions'].find { |x| x['sig'] == test_instr['sig'] }
    if gi.nil?
      @orphaned << { 'file' => file, 'error' => 'Given instruction not found in the ISA', 'comment' => test_instr }
      return 0
    end
    if gi['non_testable']
      @orphaned << { 'file' => file, 'error' => 'Given instruction is non-testable', 'comment' => test_instr }
      return 0
    end
    gi['tests'] << file
    1
  end

  def proc_test_descriptions(test_group, spec_group, file)
    cnt = 0
    test_group['description'] && split(test_group['description']).each do |tda|
      cnt += proc_test_description(tda, spec_group, file)
    end
    cnt
  end

  def proc_test_description(test_descr, spec_group, file)
    sd = spec_group['description_tests']&.find { |sda| same?(sda['assertion'], test_descr) }
    if sd.nil?
      @orphaned << { 'file' => file, 'error' => 'Given description assertion not found in the spec',
                     'comment' => test_descr }
      return 0
    end
    if sd['non_testable']
      @orphaned << { 'file' => file, 'error' => 'Given description is non-testable', 'comment' => test_descr }
      return 0
    end
    sd['tests'] << file
    1
  end

  def proc_test_exceptions(test_group, spec_group, file)
    cnt = 0
    test_group['exceptions']&.each do |te|
      cnt += proc_test_exception(te, spec_group, file)
    end
    cnt
  end

  def proc_test_exception(test_exc, spec_group, file)
    se = spec_group['exceptions_tests'].find { |x| x['exception'] == test_exc }
    if se.nil?
      @orphaned << { 'file' => file, 'error' => 'Given exception assertion not found in the spec',
                     'comment' => test_exc }
      return 0
    end
    if se['non_testable']
      @orphaned << { 'file' => file, 'error' => 'Given exception assertion is non-testable', 'comment' => test_exc }
      return 0
    end
    se['tests'] << file
    1
  end

  def proc_test_verifications(test_group, spec_group, file)
    cnt = 0
    test_group['verification']&.each do |tv|
      cnt += proc_test_verification(tv, spec_group, file)
    end
    cnt
  end

  def proc_test_verification(test_ver, spec_group, file)
    sv = spec_group['verification_tests'].find { |x| x['verification'] == test_ver }
    if sv.nil?
      @orphaned << { 'file' => file, 'error' => 'Given verification assertion not found in the spec',
                     'comment' => test_ver }
      return 0
    end
    if sv['non_testable']
      @orphaned << { 'file' => file, 'error' => 'Given verification assertion is non-testable', 'comment' => test_ver }
      return 0
    end
    sv['tests'] << file
    1
  end
end