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

require 'unparser'

# Hack to dynamically re-use the `parser` gems test suite on CI.
# The main idea is create a fake minitet runner to capture the
# signature of the examples encoded in the parsers test suite dynamically.
#
# This makes maintenance much more easier, especially on tracking new ruby
# syntax addtions.
#
# The API surface of the parser tests so far is low churn, while it may still
# make sense to provide the parser tests as an more easy to re-use data upstream.

$LOAD_PATH << Pathname.new(__dir__).parent.join('test')

test_builder = Class.new(Parser::Builders::Default)
test_builder.modernize

MODERN_ATTRIBUTES = test_builder.instance_variables.to_h do |instance_variable|
  attribute_name = instance_variable.to_s[1..].to_sym
  [attribute_name, test_builder.public_send(attribute_name)]
end

# Overwrite global scope method in the parser test suite
def default_builder_attributes
  MODERN_ATTRIBUTES.keys.to_h do |attribute_name|
    [attribute_name, Parser::Builders::Default.public_send(attribute_name)]
  end
end

class Test
  include Unparser::Adamantium, Unparser::Anima.new(
    :default_builder_attributes,
    :group_index,
    :name,
    :node,
    :parser_source,
    :rubies
  )

  EXPECT_FAILURE = {}.freeze

  def legacy_attributes
    default_builder_attributes.reject do |attribute_name, value|
      MODERN_ATTRIBUTES.fetch(attribute_name).equal?(value)
    end.to_h
  end
  memoize :legacy_attributes

  def skip_reason
    if !legacy_attributes.empty?
      "Legacy parser attributes: #{legacy_attributes}"
    elsif !allow_ruby?
      "Non targeted rubies: #{rubies.join(',')}"
    elsif validation.original_node.left?
      'Test specifies a syntax error'
    end
  end

  def success?
    validation.success?
  end

  def expect_failure?
    EXPECT_FAILURE.key?([name, group_index])
  end

  def allow_ruby?
    rubies.empty? || rubies.include?(RUBY_VERSION.split('.').take(2).join('.'))
  end

  def right(value)
    Unparser::Either::Right.new(value)
  end

  # rubocop:disable Metrics/AbcSize
  def validation
    identification   = name.to_s

    generated_source = Unparser.unparse_either(node)
      .fmap { |string| string.dup.force_encoding(parser_source.encoding).freeze }

    generated_node = generated_source.bind do |source|
      parse_either(source, identification)
    end

    Unparser::Validation.new(
      generated_node:   generated_node,
      generated_source: generated_source,
      identification:   identification,
      original_node:    parse_either(parser_source, identification).fmap { node },
      original_source:  right(parser_source)
    )
  end
  # rubocop:enable Metrics/AbcSize
  memoize :validation

  def parser
    Unparser.parser.tap do |parser|
      %w[foo bar baz].each(&parser.static_env.method(:declare))
    end
  end

  def parse_either(source, identification)
    Unparser::Either.wrap_error(Parser::SyntaxError) do
      parser.parse(Unparser.buffer(source, identification))
    end
  end
end

class Execution
  include Unparser::Anima.new(:number, :total, :test)

  def call
    skip_reason = test.skip_reason
    if skip_reason
      print('Skip', skip_reason)
      return
    end

    if test.expect_failure?
      expect_failure
    else
      expect_success
    end
  end

private

  def expect_failure
    if test.success?
      message('Expected Failure', 'but got success')
    else
      print('Expected Failure')
    end
  end

  def expect_success
    if test.success?
      print('Success')
    else
      puts(test.validation.report)
      fail message('Failure')
    end
  end

  def message(status, message = '')
    format(
      '%3<number>d/%3<total>d: %-16<status>s %<name>s[%02<group_index>d] %<message>s',
      number:      number,
      total:       total,
      status:      status,
      name:        test.name,
      group_index: test.group_index,
      message:     message
    )
  end

  def print(status, message = '')
    puts(message(status, message))
  end
end

module Minitest
  # Stub parent class
  # rubocop:disable Lint/EmptyClass
  class Test; end # Test
  # rubocop:enable Lint/EmptyClass
end # Minitest

class Extractor
  class Capture
    include Unparser::Anima.new(
      :default_builder_attributes,
      :node,
      :parser_source,
      :rubies
    )

  end

  attr_reader :tests

  def initialize
    @captures = []
    @tests    = []
  end

  def capture(**attributes)
    @captures << Capture.new(attributes)
  end

  def reset
    @captures = []
  end

  def call(name)
    reset

    TestParser.new.send(name)

    @captures.each_with_index do |capture, index|
      @tests << Test.new(name: name, group_index: index, **capture.to_h)
    end

    reset
  end
end

PARSER_PATH = Pathname.new('tmp/parser')

unless PARSER_PATH.exist?
  Kernel.system(
    *%W[
      git
      clone
      https://github.com/whitequark/parser
      #{PARSER_PATH}
    ],
    exception: true
  )
end

Dir.chdir(PARSER_PATH) do
  Kernel.system(
    *%W[
      git
      checkout
      v#{Parser::VERSION}
    ],
    exception: true
  )
  Kernel.system(*%w[git clean --force -d -X], exception: true)
end

require "./#{PARSER_PATH}/test/parse_helper"
require "./#{PARSER_PATH}/test/test_parser"

EXTRACTOR = Extractor.new

module ParseHelper
  def assert_diagnoses(*arguments); end

  def s(type, *children)
    Parser::AST::Node.new(type, children)
  end

  # rubocop:disable Metrics/ParameterLists
  def assert_parses(node, parser_source, _diagnostics = nil, rubies = [])
  EXTRACTOR.capture(
    default_builder_attributes: default_builder_attributes,
    node:                       node,
    parser_source:              parser_source,
    rubies:                     rubies
  )
  end
  # rubocop:enable Metrics/ParameterLists

  def test_clrf_line_endings(*arguments); end

  def with_versions(*arguments); end

  def assert_context(*arguments); end

  def refute_diagnoses(*arguments); end

  def assert_diagnoses_many(*arguments); end
end

TestParser.instance_methods.grep(/\Atest_/).each(&EXTRACTOR.method(:call))

EXTRACTOR.tests.sort_by(&:name).each_with_index do |test, index|
  Execution.new(number: index.succ, total: EXTRACTOR.tests.length, test: test).call
end
