# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2017-2026, by Samuel Williams.
# Copyright, 2023, by Math Ieu.
# Copyright, 2023, by Emil Tin.
# Copyright, 2025-2026, by Shopify Inc.
# Copyright, 2026, by Kyle Tate.

require "async"
require "async/clock"
require "async/queue"

require "sus/fixtures/console"

require "sus/fixtures/time/quantum"

describe Async::Task do
	let(:reactor) {Async::Reactor.new}
	
	after do
		Fiber.set_scheduler(nil)
	end
	
	with "#annotate" do
		it "can annotate the current task that has not started yet" do
			task = Async::Task.new(reactor) do |task|
				sleep
			end
			
			task.annotate("Hello World")
			
			expect(task.annotation).to be == "Hello World"
		end
		
		it "can annotate the current task that has started" do
			task = Async::Task.new(reactor) do |task|
				task.annotate("Hello World")
				
				sleep
			end
			
			expect(task.fiber).to be_nil
			
			task.run
			
			expect(task.fiber.annotation).to be == "Hello World"
		end
	end
	
	with ".yield" do
		it "can yield back to scheduler" do
			state = nil
			
			reactor.run do |task|
				child = task.async do
					state = :yielding
					Async::Task.yield
					state = :yielded
				end
				
				Fiber.scheduler.resume(child.fiber)
			end
			
			expect(state).to be == :yielded
		end
	end
	
	with "#run" do
		it "can't be invoked twice" do
			task = reactor.async do |task|
			end
			
			expect{task.run}.to raise_exception(RuntimeError, message: be =~ /already running/)
		end
	end
	
	with "#current?" do
		it "can check if it is the currently running task" do
			was_current = nil
			
			task = reactor.async do |task|
				was_current = task.current?
			end
			
			expect(was_current).to be == true
			expect(task).not.to be(:current?)
		end
		
		it "clears async_task on fiber after task finishes" do
			fiber = nil
			
			task = reactor.async do |task|
				fiber = Fiber.current
			end
			
			# After the task completes, the fiber should no longer reference the task:
			expect(fiber.async_task).to be_nil
		end
	end
	
	with "#async" do
		it "can start child async tasks" do
			parent = reactor.async do |task|
				child = task.async do
					sleep(1)
				end
				
				expect(child).not.to be_nil
				expect(child.parent).not.to be_nil
			ensure
				child.stop
			end
			
			expect(parent).not.to be_nil
			
			reactor.run
		end
		
		it "warns when using deprecated finished values" do
			verbose = $VERBOSE
			$VERBOSE = true
			task_class = Class.new(Async::Task) do
				def warn(...)
				end
			end
			
			task = task_class.new(reactor, finished: true) do
			end
			
			expect(task.status).to be == :initialized
		ensure
			$VERBOSE = verbose
		end
		
		it "can pass in arguments" do
			reactor.async do |task|
				task.async(:arg) do |task, arg|
					expect(arg).to be == :arg
				end
			end
			
			reactor.run
		end
		
		it "can set initial annotation" do
			reactor.async(annotation: "Hello World") do |task|
				expect(task.annotation).to be == "Hello World"
			end
			
			reactor.run
		end
		
		it "can raise exceptions" do
			reactor.run do
				expect do
					reactor.async do |task|
						# Ensure the wait is executed before raising the exception:
						task.yield
						
						raise "boom"
					end.wait
				end.to raise_exception(RuntimeError, message: be =~ /boom/)
			end
		end
		
		it "can raise exception after asynchronous operation" do
			task = nil
			
			expect do
				task = reactor.async do |task|
					# Let the other operation get scheduled:
					task.yield
					
					raise "boom"
				end
			end.not.to raise_exception
			
			reactor.run do
				expect do
					task.wait
				end.to raise_exception(RuntimeError, message: be =~ /boom/)
			end
		end
		
		it "can consume exceptions" do
			task = nil
			
			reactor.run do
				expect do
					task = reactor.async do |task|
						expect(task).to receive(:warn).and_return(nil)
						
						raise "boom"
					end
				end.not.to raise_exception
			end
			
			expect do
				task.wait
			end.to raise_exception(RuntimeError, message: be =~ /boom/)
		end
		
		it "won't consume non-StandardError exceptions" do
			expect do
				reactor.run do
					reactor.async do |task|
						raise SignalException.new(:TERM)
					end
				end
			end.to raise_exception(SignalException, message: be =~ /TERM/)
		end
		
		it "can't start child task after finishing" do
			task = reactor.async do |task|
			end
			
			task.wait
			
			expect do
				task.async do |task|
				end
			end.to raise_exception(RuntimeError, message: be =~ /finished/)
		end
	end
	
	with "#yield" do
		it "can yield back to reactor" do
			state = nil
			
			reactor.async do |task|
				state = :started
				task.yield
				state = :finished
			end
			
			reactor.run
			
			expect(state).to be == :finished
		end
	end
	
	with "#stop" do
		it "can't stop finished tasks" do
			task = reactor.async{}
			
			expect(task).to be(:finished?)
			expect(task).to be(:completed?)
			
			# This should be a no-op:
			task.stop
			
			expect(task).to be(:finished?)
			expect(task.status).to be == :completed
		end
		
		it "can stop a task in the initialized state" do
			task = Async::Task.new(reactor) do |task|
				sleep
			end
			
			expect(task.status).to be == :initialized
			expect(reactor.children).not.to be(:empty?)
			expect(task).not.to be(:finished?)
			
			task.stop
			
			expect(task).to be(:cancelled?)
			expect(reactor.children).to be(:empty?)
		end
		
		it "can stop a task in the initialized state with children" do
			parent = Async::Task.new(reactor) do |task|
				sleep
			end
			
			child = parent.async do |task|
				sleep
			end
			
			expect(parent.status).to be == :initialized
			# expect(child.status).to be == :running
			
			parent.stop
			
			expect(parent).to be(:cancelled?)
			expect(child).to be(:cancelled?)
			
			expect(reactor.children).to be(:empty?)
		end
		
		with "#status" do
			it "reports initialized status for new tasks" do
				task = Async::Task.new(reactor) do
					sleep 1
				end
				
				expect(task.status).to be == :initialized
			end
			
			it "reports running status for running tasks" do
				task = reactor.async do |task|
					task.yield
				end
				
				expect(task.status).to be == :running
				task.stop
			end
			
			it "reports completed status for completed tasks" do
				task = reactor.async do
					42
				end
				
				result = task.wait
				expect(task.status).to be == :completed
				expect(result).to be == 42
			end
			
			it "reports stopped status for stopped tasks" do
				task = reactor.async do |task|
					task.yield
				end
				
				task.stop
				expect(task.status).to be == :cancelled
				expect(task).to be(:cancelled?)
			end
			
			it "reports failed status for failed tasks" do
				reactor.run do
					task = reactor.async do
						raise "Test failure"
					end
					
					# Wait for the task to fail
					begin
						task.wait
					rescue
						# Expected
					end
					
					expect(task.status).to be == :failed
					expect(task.failed?).to be == true
				end
			end
			
			it "status changes reflect task lifecycle" do
				task = nil
				
				reactor.run do
					task = Async::Task.new(reactor) do |task|
						task.yield
						"completed"
					end
					
					expect(task.status).to be == :initialized
					
					# Run the task
					task.run
					expect(task.status).to be == :running
					
					# Complete the task
					result = task.wait
					expect(task.status).to be == :completed
					expect(result).to be == "completed"
				end.wait
			end
		end
		
		it "can be stopped" do
			state = nil
			
			reactor.run do
				task = reactor.async do |task|
					state = :started
					sleep(10)
					state = :finished
				end
				
				task.stop
				
				expect(task).to be(:stopped?)
			end
			
			expect(state).to be == :started
		end
		
		it "can stop nested tasks with exception handling" do
			parent = child = nil
			
			reactor.run do
				parent = reactor.async do |task|
					child = task.async do |subtask|
						# Never comes back:
						Fiber.scheduler.transfer
					end
					
					begin
						child.wait
					ensure
						child.cancel
					end
				end
				
				# Ensure the parent has a chance to run:
				Fiber.scheduler.yield
				
				parent.cancel
				parent.wait
				child.wait
			end
			
			expect(parent).to be(:cancelled?)
			expect(child).to be(:cancelled?)
		end
		
		it "can stop current task" do
			state = nil
			
			reactor.run do
				task = reactor.async do |task|
					state = :started
					task.stop
					state = :finished
				end
				
				expect(state).to be == :started
				expect(task).to be(:stopped?)
			end
		end
		
		it "can stop the parent task" do
			reactor.run do
				reactor.async do |task|
					parent_task = task
					
					reactor.async do |task|
						parent_task.stop
					end
					
					sleep(100)
				end
			end
		end
		
		it "can stop current task using exception" do
			state = nil
			
			reactor.run do
				task = reactor.async do |task|
					state = :started
					raise Async::Stop, "I'm finished."
					state = :finished
				end
				
				expect(task).to be(:stopped?)
			end
			
			expect(state).to be == :started
		end
		
		it "can stop the current task later" do
			state = nil
			task = nil
			
			reactor.run do
				task = reactor.async do |task|
					task.stop(true)
					state = :sleeping
					sleep(1)
				end
			end
			
			expect(state).to be == :sleeping
			expect(task).to be(:stopped?)
		end
		
		it "should stop direct child" do
			parent_task = child_task = nil
			
			reactor.run do
				reactor.async do |task|
					parent_task = task
					
					task.async do |task|
						child_task = task
						
						sleep(10)
					end
					
					sleep(10)
				end
				
				expect(parent_task).not.to be_nil
				expect(child_task).not.to be_nil
				
				expect(parent_task.fiber).to be(:alive?)
				expect(child_task.fiber).to be(:alive?)
				
				parent_task.stop
				
				# We need to yield here to allow the tasks to be terminated. The parent task raises an exception in the child task and adds itself to the selector ready queue. It takes at least one iteration for the parent task to exit as well:
				reactor.yield
				
				expect(parent_task).not.to be(:alive?)
				expect(child_task).not.to be(:alive?)
			end
		end
		
		it "can stop a currently resumed task" do
			parent_task = nil
			
			reactor.run do
				reactor.async do |task|
					parent_task = task
					
					Fiber.new do
						task.async do
							parent_task.stop
						end
					end.resume
					
					sleep(1)
				end
			end
			
			expect(parent_task).to be(:stopped?)
		end
		
		it "can stop nested parent" do
			parent_task = nil
			children_tasks = []
			
			reactor.run do
				reactor.async do |task|
					parent_task = task
					
					task.async do |task|
						children_tasks << task
						sleep(0.02)
					end
					
					task.async do |task|
						children_tasks << task
						sleep(0.01)
						parent_task.stop
					end
					
					task.async do |task|
						children_tasks << task
						sleep(0.02)
					end
					
					sleep(0.02)
				end
			end
			
			expect(parent_task).not.to be(:alive?)
			
			children_tasks.each do |child|
				expect(child).not.to be(:alive?)
			end
		end
		
		it "can stop the parent task which stops the stopping task" do
			condition = Async::Notification.new
			
			reactor.run do |task|
				task.async do
					condition.wait
					task.stop
				end
				
				task.async do
					sleep
				end
				
				# NOTE: Hangs only if this second task is added
				task.async do
					sleep
				end
				
				condition.signal
			end
		end
		
		it "should not remove running task" do
			top_task = middle_task = bottom_task = nil
			
			reactor.run do
				ready = Async::Queue.new
				
				reactor.async do |task|
					top_task = task
					
					top_task.async do |task|
						middle_task = task
						
						middle_task.async do |task|
							bottom_task = task
							
							ready.enqueue(true)
							
							sleep(10)
						end
						sleep(10)
					end
					sleep(10)
				end
				
				ready.dequeue
				
				bottom_task.stop
				expect(top_task.children).to be(:include?, middle_task)
				
				top_task.stop
			end
		end
		
		it "can stop resumed task" do
			items = [1, 2, 3]
			value = nil
			
			reactor.run do
				condition = Async::Condition.new
				
				producer = reactor.async do |subtask|
					while item = items.pop
						subtask.yield # (1) Fiber.yield, (3) Reactor -> producer.resume
						condition.signal(item) # (4) consumer.resume(value)
					end
				end
				
				value = condition.wait # (2) value = Fiber.yield
				producer.stop # (5) [producer is resumed already] producer.stop
			end
			
			expect(items).not.to be(:empty?)
			expect(value).to be == 3
		end
		
		it "can stop a child task with transient children" do
			parent = child = transient = nil
			
			reactor.run do |task|
				parent = task.async do |task|
					transient = task.async(transient: true) do
						sleep(1)
					end
					
					child = task.async do
						sleep(1)
					end
				end
				
				parent.wait
				expect(parent).to be(:complete?)
				
				# Should be a no-op since parent is already completed:
				parent.stop
				expect(parent).to be(:complete?)
				
				expect(transient).to be(:running?)
			end.wait
		end
		
		it "can stop a task from within with a cause" do
			error = nil
			
			cause = Async::Stop::Cause.for("boom")
			
			task = reactor.async do |task|
				begin
					task.stop(cause: cause)
				rescue Async::Stop => error
					raise
				end
			end
			
			reactor.run
			
			expect(task).to be(:stopped?)
			expect(error).to be_a(Async::Stop)
			expect(error.cause).to be == cause
		end
		
		it "can stop a task from outside with a cause" do
			error = nil
			
			cause = RuntimeError.new("boom")
			
			task = reactor.async do |task|
				begin
					task.yield
				rescue Async::Stop => error
					raise
				end
			end
			
			task.stop(cause: cause)
			
			reactor.run
			
			expect(task).to be(:stopped?)
			expect(error).to be_a(Async::Stop)
			expect(error.cause).to be == cause
		end
		
		it "defers cancellation if the target fiber cannot be raised into directly" do
			cause = RuntimeError.new("boom")
			cancelled = []
			
			task = reactor.async do |task|
				task.yield
			end
			
			reactor.define_singleton_method(:raise) do |fiber, exception, cause:|
				Kernel.raise FiberError
			end
			
			reactor.define_singleton_method(:push) do |operation|
				cancelled << operation
			end
			
			task.cancel(cause: cause)
			
			expect(cancelled.size).to be == 1
			expect(cancelled.first).to be_a(Async::Cancel::Later)
			
			singleton_class = class << reactor; self; end
			singleton_class.remove_method(:raise)
			singleton_class.remove_method(:push)
			
			cancelled.first.transfer
			reactor.run
			
			expect(task).to be(:cancelled?)
		end
		
		it "stops initialized tasks through deprecated aliases" do
			task = Async::Task.new(nil) do
			end
			
			task.__send__(:stop!)
			
			expect(task).to be(:cancelled?)
			
			task = Async::Task.new(nil) do
			end
			
			task.__send__(:stopped!)
			
			expect(task).to be(:cancelled?)
		end
		
		it "shares the generated cancellation cause with nested children" do
			cause = Async::Cancel::Cause.new("Cancelling task!")
			ready = Async::Queue.new
			parent_error = nil
			child_error = nil
			grandchild_error = nil
			
			expect(Async::Cancel::Cause).to receive(:for).with("Cancelling task!").and_return(cause)
			
			reactor.run do |task|
				parent = task.async do |parent_task|
					parent_task.async do |child_task|
						child_task.async do
							begin
								ready.enqueue(:grandchild)
								sleep
							rescue Async::Cancel => error
								grandchild_error = error
								raise
							end
						end
						
						begin
							ready.enqueue(:child)
							sleep
						rescue Async::Cancel => error
							child_error = error
							raise
						end
					end
					
					begin
						ready.enqueue(:parent)
						sleep
					rescue Async::Cancel => error
						parent_error = error
						raise
					end
				end
				
				3.times{ready.dequeue}
				parent.cancel
			end
			
			expect(parent_error.cause).to be == cause
			expect(child_error.cause).to be == cause
			expect(grandchild_error.cause).to be == cause
		end
		
		it "shares the generated cancellation cause when cancelling initialized task children" do
			cause = Async::Cancel::Cause.new("Cancelling task!")
			ready = Async::Queue.new
			child_error = nil
			grandchild_error = nil
			
			expect(Async::Cancel::Cause).to receive(:for).with("Cancelling task!").and_return(cause)
			
			reactor.run do |task|
				parent = Async::Task.new(task) do
					sleep
				end
				
				parent.async do |child_task|
					child_task.async do
						begin
							ready.enqueue(:grandchild)
							sleep
						rescue Async::Cancel => error
							grandchild_error = error
							raise
						end
					end
					
					begin
						ready.enqueue(:child)
						sleep
					rescue Async::Cancel => error
						child_error = error
						raise
					end
				end
				
				2.times{ready.dequeue}
				parent.cancel
			end
			
			expect(child_error.cause).to be == cause
			expect(grandchild_error.cause).to be == cause
		end
	end
	
	with "#sleep" do
		let(:duration) {0.01}
		
		it "can sleep for the requested duration" do
			state = nil
			
			reactor.async do |task|
				sleep(duration)
				state = :finished
			end
			
			time = Async::Clock.measure do
				reactor.run
			end
			
			# This is too unstable on travis.
			expect(time).to be_within(Sus::Fixtures::Time::QUANTUM).of(duration)
			expect(state).to be == :finished
		end
	end
	
	with "#with_timeout" do
		it "will timeout if execution takes too long" do
			state = nil
			
			Sync do |task|
				begin
					task.with_timeout(0.01) do
						state = :started
						sleep(10)
						state = :finished
					end
				rescue Async::TimeoutError
					state = :timeout
				end
			end
			
			expect(state).to be == :timeout
		end
		
		it "will timeout while getting from input" do
			input, output = IO.pipe
			error = nil
			
			Sync do |task|
				begin
					# This can invoke `io_wait`, which previously had `rescue TimeoutError`, causing the timeout to be ignored.
					task.with_timeout(0.1){input.gets}
				rescue Async::TimeoutError => error
					# Ignore.
				end
			end
			
			expect(error).to be_a(Async::TimeoutError)
		ensure
			input.close
			output.close
		end
		
		it "won't timeout if execution completes in time" do
			state = nil
			
			reactor.async do |task|
				state = :started
				task.with_timeout(0.01) do
					sleep(0.001)
					state = :finished
				end
			end
			
			reactor.run
			
			expect(state).to be == :finished
		end
		
		def sleep_forever
			while true
				sleep(1)
			end
		end
		
		it "contains useful backtrace" do
			task = Async do |task|
				expect(task).to receive(:warn).with_options(have_keys(
					exception: be_a(Async::TimeoutError),
				)).and_return(nil)
				
				task.with_timeout(0.001) do
					sleep_forever
				end
			end
			
			expect{task.wait}.to raise_exception(Async::TimeoutError)
			
			error = task.result
			expect(error.backtrace).to have_value(be =~ /sleep_forever/)
		end
	end
	
	with "#backtrace" do
		it "has a backtrace" do
			Async do
				task = Async do |task|
					sleep(1)
				end
				
				expect(task.backtrace).to have_value(be =~ /sleep/)
				
				task.stop
			end
		end
		
		with "finished task" do
			it "has no backtrace" do
				task = Async{}
				
				expect(task.backtrace).to be_nil
			end
		end
	end
	
	with "#wait" do
		it "will wait on another task to complete" do
			apples_task = reactor.async do |task|
				sleep(0.01)
				
				:apples
			end
			
			oranges_task = reactor.async do |task|
				sleep(0.01)
				
				:oranges
			end
			
			fruit_salad = reactor.async do |task|
				[apples_task.wait, oranges_task.wait]
			end
			
			reactor.run
			
			expect(fruit_salad.wait).to be == [:apples, :oranges]
		end
		
		it "will raise exceptions when checking result" do
			error_task = nil
			
			reactor.run do
				error_task = reactor.async do |task|
					expect(task).to receive(:warn).and_return(nil)
					
					raise RuntimeError, "brain not provided"
				end
				
				expect do
					error_task.wait
				end.to raise_exception(RuntimeError, message: be =~ /brain/)
			end
		end
		
		it "will propagate exceptions after async operation" do
			error_task = innocent_task = nil
			
			error_task = reactor.async(finished: false) do |task|
				raise "boom"
			end
			
			innocent_task = reactor.async(finished: false) do |task|
				error_task.wait
			end
			
			expect do
				reactor.run
			end.not.to raise_exception
			
			expect(error_task).to be(:finished?)
			expect(innocent_task).to be(:finished?)
			
			expect do
				innocent_task.wait
			end.to raise_exception(RuntimeError, message: be =~ /boom/)
		end
		
		it "will not raise exception values returned by the task" do
			error = StandardError.new
			task = reactor.async{error}
			expect(task.wait).to be == error
			expect(task.result).to be == error
		end
		
		it "raises TimeoutError if timeout expires before task completes" do
			reactor.run do
				slow_task = reactor.async do
					sleep(10)
					:done
				end
				
				expect do
					slow_task.wait(timeout: 0.001)
				end.to raise_exception(Async::TimeoutError)
				
				slow_task.cancel
			end
		end
		
		it "returns the result if the task completes within the timeout" do
			reactor.run do
				fast_task = reactor.async do
					:done
				end
				
				expect(fast_task.wait(timeout: 10)).to be == :done
			end
		end
		
		it "raises TimeoutError immediately if timeout is zero and task is not complete" do
			reactor.run do
				slow_task = reactor.async do
					sleep(10)
					:done
				end
				
				expect do
					slow_task.wait(timeout: 0)
				end.to raise_exception(Async::TimeoutError)
				
				slow_task.cancel
			end
		end
	end
	
	with "#wait_all" do
		it "will wait on all child tasks to complete" do
			results = []
			
			reactor.run do |parent|
				child1 = parent.async do |child|
					child.yield
					results << :child1
				end
				
				child2 = parent.async do |child|
					child.yield
					results << :child2
				end
				
				parent.wait_all
				results << :parent
			end
			
			expect(results).to be == [:child1, :child2, :parent]
		end
		
		it "will wait recursively on nested child tasks" do
			results = []
			
			reactor.run do |parent|
				child = parent.async do |child|
					grandchild = child.async do |grandchild|
						grandchild.yield
						results << :grandchild
					end
					
					child.yield
					results << :child
				end
				
				parent.wait_all
				results << :parent
			end
			
			expect(results).to be == [:grandchild, :child, :parent]
		end
		
		it "will skip transient tasks" do
			results = []
			
			reactor.run do |parent|
				child = parent.async do |child|
					child.yield
					results << :child
				end
				
				transient = parent.async(transient: true) do
					sleep(0.1)
					results << :transient
				end
				
				parent.wait_all
				results << :parent
			end
			
			# Transient task should not have completed
			expect(results).to be == [:child, :parent]
		end
		
		it "will handle tasks with no children" do
			reactor.run do |parent|
				result = parent.wait_all
				expect(result).to be_nil
			end
		end
		
		it "will wait on multiple levels of nesting" do
			results = []
			
			reactor.run do |parent|
				child1 = parent.async do |child|
					grandchild1 = child.async do |grandchild|
						grandchild.yield
						results << :grandchild1
					end
					child.yield
					results << :child1
				end
				
				child2 = parent.async do |child|
					grandchild2 = child.async do |grandchild|
						grandchild.yield
						results << :grandchild2
					end
					child.yield
					results << :child2
				end
				
				parent.wait_all
				results << :parent
			end
			
			# All tasks should complete in order
			expect(results).to be(:include?, :grandchild1)
			expect(results).to be(:include?, :grandchild2)
			expect(results).to be(:include?, :child1)
			expect(results).to be(:include?, :child2)
			expect(results.last).to be == :parent
		end
		
		it "returns nil when called from within the task" do
			reactor.run do |parent|
				child = parent.async do |child|
					child.yield
				end
				
				result = parent.wait_all
				expect(result).to be_nil
			end
		end
		
		it "returns the task result when called from outside" do
			parent = reactor.async do |parent|
				child = parent.async do |child|
					child.yield
				end
				:result
			end
			
			reactor.run
			
			result = parent.wait_all
			expect(result).to be == :result
		end
	end
	
	with "#result" do
		it "does not raise exception" do
			task = reactor.async do |task|
				expect(task).to receive(:warn).and_return(nil)
				
				raise "The space time converter has failed."
			end
			
			reactor.run do
				expect do
					task.wait
				end.to raise_exception(RuntimeError, message: be =~ /space time converter/)
			end
			
			expect(task.result).to be_a(RuntimeError)
		end
		
		it "does not wait for task completion" do
			task = reactor.async do |task|
				sleep(1)
			end
			
			expect(task.result).to be_nil
			
			task.stop
			
			expect(task.result).to be_nil
			expect(task).to be(:stopped?)
		end
	end
	
	with "#complete?" do
		with "running task" do
			it "is not complete?" do
				reactor.async do |task|
					expect(task).not.to be(:complete?)
				end
			end
		end
		
		with "completed task" do
			it "is complete?" do
				task = reactor.async{}
				expect(task).to be(:complete?)
			end
		end
	end
	
	with "#stopped?" do
		with "running task" do
			it "is not stopped?" do
				reactor.async do |task|
					expect(task).not.to be(:stopped?)
				end
			end
		end
		
		with "stopped task" do
			it "is stopped?" do
				child = reactor.async do |task|
					sleep(1)
				end
				
				child.stop
				
				expect(child).to be(:stopped?)
			end
		end
	end
	
	with "#children" do
		it "enumerates children in same order they are created" do
			tasks = 10.times.map do |i|
				reactor.async(annotation: "Task #{i}"){sleep(1)}
			end
			
			expect(reactor.children.each.to_a).to be == tasks
		end
	end
	
	with "#to_s" do
		it "should show running" do
			apples_task = reactor.async do
				sleep(0.1)
			end
			
			expect(apples_task.to_s).to be =~ /running/
		end
		
		it "should show complete" do
			reactor.run do
				apples_task = reactor.async do |task|
				end
				
				expect(apples_task.to_s).to be =~ /complete/
			end
		end
	end
	
	it "can gets in a task" do
		IO.pipe do |input, output|
			Async do
				Async do
					expect(input.gets).to be == "hello\n"
				end
				output.puts "hello"
			end
		end
	end
	
	with "#defer_stop" do
		it "can defer stopping a task" do
			child_task = reactor.async do |task|
				task.defer_stop do
					sleep
				end
			end
			
			reactor.run_once(0)
			
			child_task.stop
			expect(child_task).to be(:running?)
			
			child_task.stop
			expect(child_task).to be(:stopped?)
		end
		
		it "will stop the task if it was deferred" do
			condition = Async::Notification.new
			
			child_task = reactor.async do |task|
				task.defer_stop do
					condition.wait
				end
			end
			
			reactor.run_once(0)
			
			child_task.stop(true)
			expect(child_task).to be(:running?)
			
			condition.signal
			
			while child_task.running?
				reactor.run_once(0)
			end
			
			expect(child_task).to be(:stopped?)
		end
		
		it "can defer stop in a deferred stop" do
			child_task = reactor.async do |task|
				task.defer_stop do
					task.defer_stop do
						sleep
					end
				end
			end
			
			reactor.run_once(0)
			
			child_task.stop
			expect(child_task).to be(:running?)
			
			child_task.stop
			expect(child_task).to be(:stopped?)
		end
		
		it "can defer stop and exit normally" do
			child_task = reactor.async do |task|
				task.defer_stop do
					# Nothing.
				end
			end
			
			reactor.run_once(0)
			
			expect(child_task.stop_deferred?).to be == false
		end
	end
	
	with "failing task" do
		include Sus::Fixtures::Console::CapturedLogger
		
		it "logs a warning if a task fails without being waited on" do
			failed_task = nil
			
			reactor.async do |task|
				task.async do |task|
					failed_task = task
					raise "boom"
				end
			end
			
			reactor.run
			
			expect_console.to have_logged(
				severity: be == :warn,
				subject: be_equal(failed_task),
				message: be == "Task may have ended with unhandled exception."
			)
		end
		
		it "does not log a warning if a task fails and is waited on" do
			failed_task = nil
			
			reactor.async do |task|
				expect do
					task.async do |task|
						# This ensures #wait is called by the parent before proceeding to raise the exception:
						task.yield
						failed_task = task
						raise "boom"
					end.wait
				end.to raise_exception(RuntimeError, message: be =~ /boom/)
			end
			
			reactor.run
			
			expect_console.not.to have_logged(
				severity: be == :warn,
			)
		end
	end
	
	with "transient task" do
		it "is only stopped once" do
			state = nil
			
			reactor.async do |task|
				task.async(transient: true) do |task|
					task.defer_stop do
						state = :sleeping
						
						until task.stop_deferred?
							# Bounce back to the event loop several times:
							task.yield
						end
						
						# Yield one more time, which should not cause the task to be stopped again:
						task.yield
						
						state = :finished
					end
				end
			end
			
			reactor.run
			
			expect(state).to be == :finished
		end
	end
end
