{% raise("Please use `make test` or `bin/crystal` when running specs, or set the i_know_what_im_doing flag if you know what you're doing") unless env("CRYSTAL_HAS_WRAPPER") || flag?("i_know_what_im_doing") %}

Crystal::Config.path = "#{__DIR__}/../src"

require "spec"

require "compiler/requires"
require "./support/syntax"
require "./support/tempfile"
require "./support/win32"
require "./support/wasm32"

class Crystal::Program
  def reset_temp_vars
    @temp_vars.clear
  end

  def union_of(type1, type2, type3)
    union_of([type1, type2, type3] of Type).not_nil!
  end

  def proc_of(type1 : Type)
    proc_of([type1] of Type)
  end

  def proc_of(type1 : Type, type2 : Type)
    proc_of([type1, type2] of Type)
  end

  def generic_class(name, *type_vars)
    types[name].as(GenericClassType).instantiate(type_vars.to_a.map &.as(TypeVar))
  end

  def generic_module(name, *type_vars)
    types[name].as(GenericModuleType).instantiate(type_vars.to_a.map &.as(TypeVar))
  end
end

record SemanticResult,
  program : Program,
  node : ASTNode

def assert_type(str, *, inject_primitives = false, flags = nil, file = __FILE__, line = __LINE__, &)
  result = semantic(str, flags: flags, inject_primitives: inject_primitives)
  program = result.program
  expected_type = with program yield program
  node = result.node
  if node.is_a?(Expressions)
    node = node.last
  end
  node.type.should eq(expected_type), file: file, line: line
  result
end

def semantic(code : String, wants_doc = false, inject_primitives = false, flags = nil, filename = nil)
  warnings = WarningCollection.new
  node = parse(code, wants_doc: wants_doc, filename: filename, warnings: warnings)
  node = inject_primitives(node) if inject_primitives
  semantic node, warnings: warnings, wants_doc: wants_doc, flags: flags
end

private def inject_primitives(node : ASTNode)
  req = Crystal::Require.new("primitives")
  case node
  when Crystal::Expressions
    node.expressions.unshift req
    node
  when Crystal::Nop
    node
  else
    Crystal::Expressions.new [req, node] of ASTNode
  end
end

def semantic(node : ASTNode, *, warnings = nil, wants_doc = false, flags = nil)
  program = new_program
  program.warnings = warnings if warnings
  program.flags.concat(flags.split) if flags
  program.wants_doc = wants_doc
  node = program.normalize node
  node = program.semantic node
  SemanticResult.new(program, node)
end

def top_level_semantic(code : String, wants_doc = false, inject_primitives = false)
  node = parse(code, wants_doc: wants_doc)
  node = inject_primitives(node) if inject_primitives
  top_level_semantic node, wants_doc: wants_doc
end

def top_level_semantic(node : ASTNode, wants_doc = false)
  program = new_program
  program.wants_doc = wants_doc
  node = program.normalize node
  node, _ = program.top_level_semantic node
  SemanticResult.new(program, node)
end

def assert_normalize(from, to, flags = nil, *, filename = nil, file = __FILE__, line = __LINE__)
  program = new_program
  program.flags.concat(flags.split) if flags
  from_nodes = parse(from, filename: filename)
  to_nodes = program.normalize(from_nodes)
  to_nodes_str = to_nodes.to_s.strip
  to_nodes_str.should eq(to.strip), file: file, line: line

  # first idempotency check: the result should be fully normalized
  to_nodes_str2 = program.normalize(to_nodes).to_s.strip
  unless to_nodes_str2 == to_nodes_str
    fail "Idempotency failed:\nBefore: #{to_nodes_str.inspect}\nAfter:  #{to_nodes_str2.inspect}", file: file, line: line
  end

  # second idempotency check: if the normalizer mutates the original node,
  # further normalizations should not produce a different result
  program.reset_temp_vars
  to_nodes_str2 = program.normalize(from_nodes).to_s.strip
  unless to_nodes_str2 == to_nodes_str
    fail "Idempotency failed:\nBefore: #{to_nodes_str.inspect}\nAfter:  #{to_nodes_str2.inspect}", file: file, line: line
  end

  to_nodes
end

def assert_expand(from : String, to, *, flags = nil, file = __FILE__, line = __LINE__)
  node = Parser.parse(from)
  if node.is_a?(Expressions)
    node = node.last
  end
  assert_expand node, to, flags: flags, file: file, line: line
end

def assert_expand(from_nodes : ASTNode, to, *, flags = nil, file = __FILE__, line = __LINE__)
  assert_expand(from_nodes, flags: flags, file: file, line: line) do |to_nodes|
    to_nodes.to_s.strip.should eq(to.strip), file: file, line: line
  end
end

def assert_expand(from_nodes : ASTNode, *, flags = nil, file = __FILE__, line = __LINE__, &)
  program = new_program
  program.flags.concat(flags.split) if flags
  to_nodes = LiteralExpander.new(program).expand(from_nodes)
  yield to_nodes, program
end

def assert_expand_named(from : String, to, *, generic = nil, flags = nil, filename = nil, file = __FILE__, line = __LINE__)
  program = new_program
  program.flags.concat(flags.split) if flags
  from_nodes = parse(from, filename: filename)
  generic_type = generic.path if generic
  case from_nodes
  when ArrayLiteral
    to_nodes = LiteralExpander.new(program).expand_named(from_nodes, generic_type)
  when HashLiteral
    to_nodes = LiteralExpander.new(program).expand_named(from_nodes, generic_type)
  else
    fail "Expected: ArrayLiteral | HashLiteral, got: #{from_nodes.class}", file: file, line: line
  end
  to_nodes.to_s.strip.should eq(to.strip), file: file, line: line
end

def assert_error(str, message = nil, *, inject_primitives = false, flags = nil, file = __FILE__, line = __LINE__)
  expect_raises TypeException, message, file, line do
    semantic str, inject_primitives: inject_primitives, flags: flags
  end
end

def assert_no_errors(*args, **opts)
  semantic(*args, **opts)
end

def warnings_result(code)
  semantic(code).program.warnings.infos
end

def assert_warning(code, message, *, file = __FILE__, line = __LINE__)
  warning_failures = warnings_result(code)
  warning_failures.size.should eq(1), file: file, line: line
  warning_failures[0].should contain(message), file: file, line: line
end

def assert_no_warning(code, *, file = __FILE__, line = __LINE__)
  warning_failures = warnings_result(code)
  warning_failures.should be_empty, file: file, line: line
end

def assert_macro(macro_body, expected, args = nil, *, expected_pragmas = nil, flags = nil, file = __FILE__, line = __LINE__)
  assert_macro(macro_body, expected, expected_pragmas: expected_pragmas, flags: flags, file: file, line: line) { args }
end

def assert_macro(macro_body, expected, *, expected_pragmas = nil, flags = nil, file = __FILE__, line = __LINE__, &)
  program, a_macro, call = prepare_macro_call(macro_body, flags) { |program| yield program }
  result, result_pragmas = program.expand_macro(a_macro, call, program, program)
  result = result.chomp(';')
  result.should eq(expected), file: file, line: line
  result_pragmas.should eq(expected_pragmas), file: file, line: line if expected_pragmas
end

def assert_macro_error(macro_body, message = nil, args = nil, *, flags = nil, file = __FILE__, line = __LINE__)
  assert_macro_error(macro_body, message, flags: flags, file: file, line: line) { args }
end

def assert_macro_error(macro_body, message = nil, *, flags = nil, file = __FILE__, line = __LINE__, &)
  program, a_macro, call = prepare_macro_call(macro_body, flags) { |program| yield program }
  expect_raises(Crystal::TypeException, message, file: file, line: line) do
    program.expand_macro(a_macro, call, program, program)
  end
end

def prepare_macro_call(macro_body, flags = nil, &)
  program = new_program
  flags = flags.split if flags.is_a?(String)
  program.flags.concat(flags) if flags
  program.top_level_semantic_complete = true
  args = yield program

  macro_params = args.try &.keys.join(", ")
  call_args = [] of ASTNode
  call_args.concat(args.values) if args

  a_macro = Parser.parse("macro foo(#{macro_params});#{macro_body};end").as(Macro)
  call = Call.new("", call_args)

  {program, a_macro, call}
end

def codegen(code, *, inject_primitives = true, single_module = false, debug = Crystal::Debug::None, filename = __FILE__)
  result = semantic code, inject_primitives: inject_primitives, filename: filename
  result.program.codegen(result.node, single_module: single_module, debug: debug)[""].mod
end

def compile(*codes, prelude = "empty", debug = Crystal::Debug::None, target = nil)
  sources = codes.map_with_index do |code, index|
    Compiler::Source.new("file#{index}.cr", code)
  end.to_a

  compiler = create_spec_compiler
  compiler.prelude = prelude
  compiler.debug = debug
  compiler.stdout = IO::Memory.new # don't print anything

  if target
    compiler.cross_compile = true # skip linker
    compiler.codegen_target = Crystal::Codegen::Target.new(target)
  end

  with_temp_executable("crystal-spec-output") do |output_filename|
    compiler.compile(sources, output_filename)
  end
end

private def new_program
  program = Program.new
  program.color = false
  apply_program_flags(program.flags)
  program
end

# Use CRYSTAL_SPEC_COMPILER_FLAGS env var to run the compiler specs
# against a compiler with the specified options.
# Separate flags with a space.
# Using CRYSTAL_SPEC_COMPILER_FLAGS="foo bar" will mimic -Dfoo -Dbar options.
private def apply_program_flags(target)
  ENV["CRYSTAL_SPEC_COMPILER_FLAGS"]?.try { |f| target.concat(f.split) }
end

private def spec_compiler_threads
  ENV["CRYSTAL_SPEC_COMPILER_THREADS"]?.try(&.to_i)
end

private def encode_program_flags : String
  program_flags_options.join(' ')
end

def program_flags_options : Array(String)
  f = [] of String
  apply_program_flags(f)
  options = f.map { |x| "-D#{x}" }
  if (n_threads = spec_compiler_threads)
    options << "--threads=#{n_threads}"
  end
  options
end

class Crystal::SpecRunOutput
  @output : String

  def initialize(@output)
  end

  def to_string
    @output
  end

  delegate to_i, to_i64, to_u64, to_f, to_f32, to_f64, to: @output

  def to_b
    @output == "true"
  end
end

def create_spec_compiler
  compiler = Compiler.new
  if (n_threads = spec_compiler_threads)
    compiler.n_threads = n_threads
  end

  compiler
end

def run(code, filename : String? = nil, inject_primitives = true, debug = Crystal::Debug::None, flags = nil, *, file = __FILE__) : LLVM::GenericValue | SpecRunOutput
  if inject_primitives
    code = %(require "primitives"\n#{code})
  end

  # Code that requires the prelude doesn't run in LLVM's MCJIT
  # because of missing linked functions (which are available
  # in the current executable!), so instead we compile
  # the program and run it, printing the last
  # expression and using that to compare the result.
  if code.includes?(%(require "prelude"))
    ast = Parser.parse(code).as(Expressions)
    last = ast.expressions.last
    assign = Assign.new(Var.new("__tempvar"), last)
    call = Call.new("print", Var.new("__tempvar"))
    exps = Expressions.new([assign, call] of ASTNode)
    ast.expressions[-1] = exps
    code = ast.to_s

    compiler = create_spec_compiler
    compiler.debug = debug
    compiler.flags.concat flags if flags
    apply_program_flags(compiler.flags)

    with_temp_executable("crystal-spec-output", file: file) do |output_filename|
      compiler.compile Compiler::Source.new("spec", code), output_filename

      output = Process.capture(output_filename)
      return SpecRunOutput.new(output)
    end
  else
    program = new_program
    program.flags.concat(flags) if flags
    program.run(code, filename: filename, debug: debug)
  end
end

def run(code, return_type : T.class, filename : String? = nil, inject_primitives = true, debug = Crystal::Debug::None, flags = nil, *, file = __FILE__) forall T
  if inject_primitives
    code = %(require "primitives"\n#{code})
  end

  if code.includes?(%(require "prelude"))
    fail "TODO: support the prelude in typed codegen specs", file: file
  else
    program = new_program
    program.flags.concat(flags) if flags
    program.run(code, return_type: T, filename: filename, debug: debug)
  end
end

def test_c(c_code, crystal_code, *, file = __FILE__, &)
  with_temp_c_object_file(c_code, file: file) do |o_filename|
    yield run(<<-CRYSTAL)
      require "prelude"

      @[Link(ldflags: #{o_filename.inspect})]
      #{crystal_code}
      CRYSTAL
  end
end
