# frozen_string_literal: false
require 'test/unit'
require 'fiber'
EnvUtil.suppress_warning {require 'continuation'}
require 'tmpdir'

class TestFiber < Test::Unit::TestCase
  def test_normal
    assert_equal(:ok2,
      Fiber.new{|e|
        assert_equal(:ok1, e)
        Fiber.yield :ok2
      }.resume(:ok1)
    )
    assert_equal([:a, :b], Fiber.new{|a, b| [a, b]}.resume(:a, :b))
  end

  def test_argument
    assert_equal(4, Fiber.new {|i=4| i}.resume)
  end

  def test_term
    assert_equal(:ok, Fiber.new{:ok}.resume)
    assert_equal([:a, :b, :c, :d, :e],
      Fiber.new{
        Fiber.new{
          Fiber.new{
            Fiber.new{
              [:a]
            }.resume + [:b]
          }.resume + [:c]
        }.resume + [:d]
      }.resume + [:e])
  end

  def test_many_fibers
    max = 10_000
    assert_equal(max, max.times{
      Fiber.new{}
    })
    GC.start # force collect created fibers
    assert_equal(max,
      max.times{|i|
        Fiber.new{
        }.resume
      }
    )
    GC.start # force collect created fibers
  end

  def test_many_fibers_with_threads
    assert_normal_exit <<-SRC, timeout: 60
      max = 1000
      @cnt = 0
      (1..100).map{|ti|
        Thread.new{
          max.times{|i|
            Fiber.new{
              @cnt += 1
            }.resume
          }
        }
      }.each{|t|
        t.join
      }
    SRC
  end

  def test_error
    assert_raise(ArgumentError){
      Fiber.new # Fiber without block
    }
    f = Fiber.new{}
    Thread.new{
      assert_raise(FiberError){ # Fiber yielding across thread
        f.resume
      }
    }.join
    assert_raise(FiberError){
      f = Fiber.new{}
      f.resume
      f.resume
    }
    assert_raise(RuntimeError){
      Fiber.new{
        @c = callcc{|c| @c = c}
      }.resume
      @c.call # cross fiber callcc
    }
    assert_raise(RuntimeError){
      Fiber.new{
        raise
      }.resume
    }
    assert_raise(FiberError){
      Fiber.yield
    }
    assert_raise(FiberError){
      fib = Fiber.new{
        fib.resume
      }
      fib.resume
    }
    assert_raise(FiberError){
      fib = Fiber.new{
        Fiber.new{
          fib.resume
        }.resume
      }
      fib.resume
    }
  end

  def test_return
    assert_raise(LocalJumpError){
      Fiber.new do
        return
      end.resume
    }
  end

  def test_throw
    assert_raise(UncaughtThrowError){
      Fiber.new do
        throw :a
      end.resume
    }
  end

  def test_transfer
    ary = []
    f2 = nil
    f1 = Fiber.new{
      ary << f2.transfer(:foo)
      :ok
    }
    f2 = Fiber.new{
      ary << f1.transfer(:baz)
      :ng
    }
    assert_equal(:ok, f1.transfer)
    assert_equal([:baz], ary)
  end

  def test_tls
    #
    def tvar(var, val)
      old = Thread.current[var]
      begin
        Thread.current[var] = val
        yield
      ensure
        Thread.current[var] = old
      end
    end

    fb = Fiber.new {
      assert_equal(nil, Thread.current[:v]); tvar(:v, :x) {
      assert_equal(:x,  Thread.current[:v]);   Fiber.yield
      assert_equal(:x,  Thread.current[:v]); }
      assert_equal(nil, Thread.current[:v]); Fiber.yield
      raise # unreachable
    }

    assert_equal(nil, Thread.current[:v]); tvar(:v,1) {
    assert_equal(1,   Thread.current[:v]);   tvar(:v,3) {
    assert_equal(3,   Thread.current[:v]);     fb.resume
    assert_equal(3,   Thread.current[:v]);   }
    assert_equal(1,   Thread.current[:v]); }
    assert_equal(nil, Thread.current[:v]); fb.resume
    assert_equal(nil, Thread.current[:v]);
  end

  def test_alive
    fib = Fiber.new{Fiber.yield}
    assert_equal(true, fib.alive?)
    fib.resume
    assert_equal(true, fib.alive?)
    fib.resume
    assert_equal(false, fib.alive?)
  end

  def test_resume_self
    f = Fiber.new {f.resume}
    assert_raise(FiberError, '[ruby-core:23651]') {f.transfer}
  end

  def test_fiber_transfer_segv
    assert_normal_exit %q{
      require 'fiber'
      f2 = nil
      f1 = Fiber.new{ f2.resume }
      f2 = Fiber.new{ f1.resume }
      f1.transfer
    }, '[ruby-dev:40833]'
    assert_normal_exit %q{
      require 'fiber'
      Fiber.new{}.resume
      1.times{Fiber.current.transfer}
    }
  end

  def test_resume_root_fiber
    Thread.new do
      assert_raise(FiberError) do
        Fiber.current.resume
      end
    end.join
  end

  def test_gc_root_fiber
    bug4612 = '[ruby-core:35891]'

    assert_normal_exit %q{
      require 'fiber'
      GC.stress = true
      Thread.start{ Fiber.current; nil }.join
      GC.start
    }, bug4612
  end

  def test_mark_fiber
    bug13875 = '[ruby-core:82681]'

    assert_normal_exit %q{
      GC.stress = true
      up = 1.upto(10)
      down = 10.downto(1)
      up.zip(down) {|a, b| a + b == 11 or fail 'oops'}
    }, bug13875
  end

  def test_no_valid_cfp
    bug5083 = '[ruby-dev:44208]'
    assert_equal([], Fiber.new(&Module.method(:nesting)).resume, bug5083)
    assert_instance_of(Class, Fiber.new(&Class.new.method(:undef_method)).resume(:to_s), bug5083)
  end

  def test_prohibit_resume_transferred_fiber
    assert_raise(FiberError){
      root_fiber = Fiber.current
      f = Fiber.new{
        root_fiber.transfer
      }
      f.transfer
      f.resume
    }
    assert_raise(FiberError){
      g=nil
      f=Fiber.new{
        g.resume
        g.resume
      }
      g=Fiber.new{
        f.resume
        f.resume
      }
      f.transfer
    }
  end

  def test_fork_from_fiber
    skip 'fork not supported' unless Process.respond_to?(:fork)
    pid = nil
    bug5700 = '[ruby-core:41456]'
    assert_nothing_raised(bug5700) do
      Fiber.new do
        pid = fork do
          xpid = nil
          Fiber.new {
            xpid = fork do
              # enough to trigger GC on old root fiber
              count = 10000
              count = 1000 if /openbsd/i =~ RUBY_PLATFORM
              count.times do
                Fiber.new {}.transfer
                Fiber.new { Fiber.yield }
              end
              exit!(0)
            end
          }.transfer
          _, status = Process.waitpid2(xpid)
          exit!(status.success?)
        end
      end.resume
    end
    pid, status = Process.waitpid2(pid)
    assert_equal(0, status.exitstatus, bug5700)
    assert_equal(false, status.signaled?, bug5700)
  end

  def test_exit_in_fiber
    bug5993 = '[ruby-dev:45218]'
    assert_nothing_raised(bug5993) do
      Thread.new{ Fiber.new{ Thread.exit }.resume; raise "unreachable" }.join
    end
  end

  def test_fatal_in_fiber
    assert_in_out_err(["-r-test-/fatal/rb_fatal", "-e", <<-EOS], "", [], /ok/)
      Fiber.new{
        rb_fatal "ok"
      }.resume
      puts :ng # unreachable.
    EOS
  end

  def invoke_rec script, vm_stack_size, machine_stack_size, use_length = true
    env = {}
    env['RUBY_FIBER_VM_STACK_SIZE'] = vm_stack_size.to_s if vm_stack_size
    env['RUBY_FIBER_MACHINE_STACK_SIZE'] = machine_stack_size.to_s if machine_stack_size
    out, _ = Dir.mktmpdir("test_fiber") {|tmpdir|
      EnvUtil.invoke_ruby([env, '-e', script], '', true, true, chdir: tmpdir, timeout: 30)
    }
    use_length ? out.length : out
  end

  def test_stack_size
    h_default = eval(invoke_rec('p RubyVM::DEFAULT_PARAMS', nil, nil, false))
    h_0 = eval(invoke_rec('p RubyVM::DEFAULT_PARAMS', 0, 0, false))
    h_large = eval(invoke_rec('p RubyVM::DEFAULT_PARAMS', 1024 * 1024 * 10, 1024 * 1024 * 10, false))

    assert_operator(h_default[:fiber_vm_stack_size], :>, h_0[:fiber_vm_stack_size])
    assert_operator(h_default[:fiber_vm_stack_size], :<, h_large[:fiber_vm_stack_size])
    assert_operator(h_default[:fiber_machine_stack_size], :>=, h_0[:fiber_machine_stack_size])
    assert_operator(h_default[:fiber_machine_stack_size], :<=, h_large[:fiber_machine_stack_size])

    # check VM machine stack size
    script = '$stdout.sync=true; def rec; print "."; rec; end; Fiber.new{rec}.resume'
    size_default = invoke_rec script, nil, nil
    assert_operator(size_default, :>, 0)
    size_0 = invoke_rec script, 0, nil
    assert_operator(size_default, :>, size_0)
    size_large = invoke_rec script, 1024 * 1024 * 10, nil
    assert_operator(size_default, :<, size_large)

    return if /mswin|mingw/ =~ RUBY_PLATFORM

    # check machine stack size
    # Note that machine stack size may not change size (depend on OSs)
    script = '$stdout.sync=true; def rec; print "."; 1.times{1.times{1.times{rec}}}; end; Fiber.new{rec}.resume'
    vm_stack_size = 1024 * 1024
    size_default = invoke_rec script, vm_stack_size, nil
    size_0 = invoke_rec script, vm_stack_size, 0
    assert_operator(size_default, :>=, size_0)
    size_large = invoke_rec script, vm_stack_size, 1024 * 1024 * 10
    assert_operator(size_default, :<=, size_large)
  end

  def test_separate_lastmatch
    bug7678 = '[ruby-core:51331]'
    /a/ =~ "a"
    m1 = $~
    m2 = nil
    Fiber.new do
      /b/ =~ "b"
      m2 = $~
    end.resume
    assert_equal("b", m2[0])
    assert_equal(m1, $~, bug7678)
  end

  def test_separate_lastline
    bug7678 = '[ruby-core:51331]'
    $_ = s1 = "outer"
    s2 = nil
    Fiber.new do
      s2 = "inner"
    end.resume
    assert_equal("inner", s2)
    assert_equal(s1, $_, bug7678)
  end

  def test_new_symbol_proc
    bug = '[ruby-core:80147] [Bug #13313]'
    assert_ruby_status([], "#{<<-"begin;"}\n#{<<-'end;'}", bug)
    begin;
      exit("1" == Fiber.new(&:to_s).resume(1))
    end;
  end

  def test_to_s
    f = Fiber.new do
      assert_match(/resumed/, f.to_s)
      Fiber.yield
    end
    assert_match(/created/, f.to_s)
    f.resume
    assert_match(/suspended/, f.to_s)
    f.resume
    assert_match(/terminated/, f.to_s)
    assert_match(/resumed/, Fiber.current.to_s)
  end

  def test_create_fiber_in_new_thread
    ret = Thread.new{
      Thread.new{
        Fiber.new{Fiber.yield :ok}.resume
      }.value
    }.value
    assert_equal :ok, ret, '[Bug #14642]'
  end

  def test_machine_stack_gc
    assert_normal_exit <<-RUBY, '[Bug #14561]', timeout: 10
      enum = Enumerator.new { |y| y << 1 }
      thread = Thread.new { enum.peek }
      thread.join
      sleep 5     # pause until thread cache wait time runs out. Native thread exits.
      GC.start
    RUBY
  end
end
