# frozen_string_literal: true
gem "minitest", "6.0.6"
require "minitest/autorun"
require "timeout"

module CounterLab
  class OwnedCounter
    def initialize
      @mailbox = Queue.new
      @thread = Thread.new do
        value = 0
        loop do
          operation, argument, reply = @mailbox.pop
          case operation
          when :get then reply << value
          when :set then value = argument; reply << value
          when :increment then value += 1; reply << value
          when :stop then reply << value; break
          else reply << :unsupported
          end
        end
      end
    end
    def call(operation, argument = nil)
      reply = Queue.new
      @mailbox << [operation, argument, reply]
      reply.pop
    end
    def close
      call(:stop)
      @thread.join
    end
  end

  module_function
  # Barriers force both clients to read before either writes.
  def split_updates(read, write)
    ready, go = Queue.new, Queue.new
    threads = 2.times.map do
      Thread.new do
        old = read.call
        ready << true
        go.pop
        write.call(old + 1)
      end
    end
    2.times { ready.pop }
    2.times { go << true }
    threads.each(&:value)
  ensure
    threads&.each { |thread| thread.kill if thread.alive? }
  end
end

class CounterLabTest < Minitest::Test
  def test_shared_state_loses_update_with_split_operation
    state = {value: 0}
    Timeout.timeout(5) do
      CounterLab.split_updates(-> { state[:value] }, ->(n) { state[:value] = n })
    end
    assert_equal 1, state[:value]
  end
  def test_mutex_around_whole_operation_preserves_both_updates
    mutex = Mutex.new
    state = {value: 0}
    threads = 2.times.map { Thread.new { mutex.synchronize { state[:value] += 1 } } }
    threads.each(&:value)
    assert_equal 2, state[:value]
  end
  def test_single_owner_still_loses_update_with_two_messages
    counter = CounterLab::OwnedCounter.new
    Timeout.timeout(5) do
      CounterLab.split_updates(-> { counter.call(:get) }, ->(n) { counter.call(:set, n) })
      assert_equal 1, counter.call(:get)
    end
  ensure
    counter&.close
  end
  def test_single_atomic_message_preserves_updates
    counter = CounterLab::OwnedCounter.new
    threads = 20.times.map { Thread.new { counter.call(:increment) } }
    Timeout.timeout(5) { threads.each(&:value) }
    assert_equal 20, counter.call(:get)
  ensure
    counter&.close
  end
end
