# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025-2026, by Samuel Williams.
# Copyright, 2026, by Fletcher Dares.

require "io/event"
require "io/event/test_scheduler"

return unless defined?(IO::Event::WorkerPool)

describe IO::Event::WorkerPool do
	with "an instance" do
		let(:worker_pool) {subject.new}
		
		after do
			worker_pool&.close
		end
		
		it "can create a worker pool" do
			expect(worker_pool).to be_a(IO::Event::WorkerPool)
		end
		
		it "provides statistics" do
			# Force initialization by calling a method on the pool
			pool = worker_pool # This should trigger initialization
			statistics = pool.statistics
			
			expect(statistics).to be_a(Hash)
			expect(statistics).to have_keys(
				current_worker_count: be_a(Integer),
				maximum_worker_count: be == 1,
				current_queue_size: be == 0,
				shutdown: be == false
			)
		end
		
		it "can close the worker pool" do
			pool = worker_pool
			
			# Check that it's not shut down initially
			expect(pool.statistics[:shutdown]).to be == false
			
			# Close the pool
			result = pool.close
			expect(result).to be_nil
			
			# Check that it's now shut down
			expect(pool.statistics[:shutdown]).to be == true
			expect(pool.statistics[:current_worker_count]).to be == 0
		end
		
		it "can close the worker pool multiple times safely" do
			pool = worker_pool
			
			# Close the pool twice
			pool.close
			pool.close
			
			# Should still be shut down
			expect(pool.statistics[:shutdown]).to be == true
		end
	end
	
	with IO::Event::TestScheduler do
		let(:scheduler) {IO::Event::TestScheduler.new}
		
		it "can create a test scheduler" do
			expect(scheduler).to be_a(IO::Event::TestScheduler)
			expect(scheduler.worker_pool).to be_a(IO::Event::WorkerPool)
		end
		
		it "interrupts IO::Buffer.copy operations larger than 1MiB" do
			skip "IO::Buffer not available" unless defined?(IO::Buffer)
			
			# Create buffers larger than 1MiB to trigger GVL release
			buffer_size = 2 * 1024 * 1024  # 2MiB
			source = IO::Buffer.new(buffer_size)
			destination = IO::Buffer.new(buffer_size)
			
			# Fill source buffer with some data
			source.clear("A".ord)
			worker_pool = nil
			
			Thread.new do
				Fiber.set_scheduler(scheduler)
				worker_pool = scheduler.worker_pool
				
				# Perform the large copy operation in a scheduled fiber
				Fiber.schedule do
					destination.copy(source, 0, buffer_size, 0)
				end
			end.join
			
			# Confirm that the copy worked:
			expect(destination.get_string(0, 10)).to be == "AAAAAAAAAA"
			
			expect(worker_pool.statistics[:call_count]).to be > 0
			expect(worker_pool.statistics[:completed_count]).to be > 0
			inform worker_pool.statistics
		end
	end
	
	with "cancellable busy operation" do
		let(:scheduler) {IO::Event::TestScheduler.new}
		
		it "can perform a busy operation that completes normally" do
			start_time = Time.now
			result = IO::Event::WorkerPool.busy(duration: 0.1)
			end_time = Time.now
			elapsed = end_time - start_time
			
			expect(result).to be_a(Hash)
			expect(result[:cancelled]).to be == false
			expect(result[:result]).to be == :completed
		end
		
		it "can perform a busy operation with different durations" do
			result = IO::Event::WorkerPool.busy(duration: 0.05)
			
			expect(result).to be_a(Hash)
			expect(result[:cancelled]).to be == false
			expect(result[:result]).to be == :completed
			expect(result[:duration]).to be == 0.05
		end
		
		it "keeps blocked fibers alive during garbage collection" do
			result = nil
			
			Thread.new do
				Fiber.set_scheduler(scheduler)
				
				gc_thread = Thread.new do
					sleep(0.02)
					GC.start(full_mark: true, immediate_sweep: true)
				end
				
				Fiber.schedule do
					result = IO::Event::WorkerPool.busy(duration: 0.1)
				end
				
				scheduler.run
				gc_thread.value
			ensure
				Fiber.set_scheduler(nil)
				scheduler.close
			end.value
			
			expect(result).to have_keys(
				cancelled: be == false,
				result: be == :completed
			)
		end
		
		it "can cancel a busy operation using unblock function" do
			# This tests the cancellation mechanism through rb_thread_call_without_gvl
			completed = false
			thread = Thread.new do
				start_time = Time.now
				result = IO::Event::WorkerPool.busy(duration: 1.0)  # Long operation
				end_time = Time.now
				elapsed = end_time - start_time
				completed = true
				
				{result: result, elapsed: elapsed}
			end
			
			# Let it start, then kill the thread (which should trigger the unblock function)
			sleep(0.1)
			thread.kill
			thread.join(0.5)  # Wait up to 0.5s for thread to finish
			
			# The operation should have been interrupted before completion
			expect(completed).to be == false
		end
		
		it "can be cancelled when executed in a worker pool" do
			result = nil
			elapsed = nil
			error = nil
			
			Thread.new do
				Fiber.set_scheduler(scheduler)
				
				busy_fiber = Fiber.schedule do
					start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
					result = IO::Event::WorkerPool.busy(duration: 2.0)
				rescue Interrupt => error
					# Ignore.
				ensure
					end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
					elapsed = end_time - start_time
				end
				
				Fiber.schedule do
					sleep(0.5)
					Fiber.scheduler.fiber_interrupt(busy_fiber, StandardError)
				end
			end.join
			
			expect(result[:cancelled]).to be == true
			expect(elapsed).to be < 1.0
			expect(error).to be_nil
		end
		
		it "can be cancelled before even starting" do
			result = nil
			
			Thread.new do
				Fiber.set_scheduler(scheduler)
				
				busy_fiber = Fiber.schedule do
					result = IO::Event::WorkerPool.busy(duration: 2.0)
				end
				
				Fiber.schedule do
					Fiber.scheduler.fiber_interrupt(busy_fiber, StandardError)
				end
			end.join
			
			expect(result).to have_keys(
				cancelled: be == true,
				result: be == :exception,
				exception: be_a(StandardError)
			)
		end
	end
	
	with "scheduler that rescues internally in block" do
		# A scheduler whose block method raises on the first call
		# and rescues on a subsequent call clearing ec->errinfo.
		class RescuingBlockScheduler < IO::Event::TestScheduler
			attr_reader :rescued
			
			def initialize(...)
				super
				@block_call_count = 0
			end
			
			def block(blocker, timeout = nil)
				@block_call_count += 1
				
				raise StandardError, "first block failure" if @block_call_count == 1
				
				begin
					raise StandardError, "internal block error"
				rescue
					@rescued = true
				end
				super
			end
		end
		
		it "preserves the original exception through block rescue" do
			result = nil
			rescued = false
			
			Thread.new do
				scheduler = RescuingBlockScheduler.new
				Fiber.set_scheduler(scheduler)
				
				Fiber.schedule do
					result = IO::Event::WorkerPool.busy(duration: 2.0)
				end
				rescued = scheduler.rescued
			ensure
				Fiber.set_scheduler(nil)
				scheduler&.close
			end.value
			
			expect(result).to be_a(Hash)
			expect(result[:result]).to be == :exception
			expect(result[:exception]).to be_a(StandardError)
			expect(result[:exception].message).to be == "first block failure"
			expect(rescued).to be == true
		end
	end
	
	with "scheduler that throws from block" do
		class ThrowingBlockScheduler < IO::Event::TestScheduler
			attr_reader :rescued
			
			def initialize(...)
				super
				@block_call_count = 0
			end
			
			def block(...)
				@block_call_count += 1
				
				throw :worker_pool_escape, :expected if @block_call_count == 1
				
				begin
					raise StandardError, "internal block error"
				rescue
					@rescued = true
				end
				
				super
			end
		end
		
		it "preserves non-exception control flow" do
			result = nil
			rescued = false
			
			Thread.new do
				scheduler = ThrowingBlockScheduler.new
				Fiber.set_scheduler(scheduler)
				
				Fiber.schedule do
					result = catch(:worker_pool_escape) do
						IO::Event::WorkerPool.busy(duration: 2.0)
						:missing
					end
				end
				rescued = scheduler.rescued
			ensure
				Fiber.set_scheduler(nil)
				scheduler&.close
			end.value
			
			expect(result).to be == :expected
			expect(rescued).to be == true
		end
	end
end
