# frozen_string_literal: true
# rubocop:todo all

require 'spec_helper'

class SessionTransactionSpecError < StandardError; end

describe Mongo::Session do
  require_wired_tiger
  min_server_fcv '4.0'
  require_topology :replica_set, :sharded

  let(:session) do
    authorized_client.start_session(session_options)
  end

  let(:session_options) do
    {}
  end

  let(:collection) do
    authorized_client['session-transaction-test']
  end

  before do
    collection.delete_many
  end

  describe 'start_transaction' do
    context 'when topology is sharded and server is < 4.2' do
      max_server_fcv '4.1'
      require_topology :sharded

      it 'raises an error' do
        expect { session.start_transaction }.to raise_error(Mongo::Error::TransactionsNotSupported, /sharded transactions require server version/)
      end
    end
  end

  describe '#abort_transaction' do
    require_topology :replica_set

    context 'when a non-Mongo error is raised' do
      before do
        collection.insert_one({foo: 1})
      end

      it 'propagates the exception and sets state to transaction aborted' do
        session.start_transaction
        collection.insert_one({foo: 1}, session: session)
        expect(session).to receive(:write_with_retry).and_raise(SessionTransactionSpecError)
        expect do
          session.abort_transaction
        end.to raise_error(SessionTransactionSpecError)
        expect(session.send(:within_states?, Mongo::Session::TRANSACTION_ABORTED_STATE)).to be true

        # Since we failed abort_transaction call, the transaction is still
        # outstanding. It will cause subsequent tests to stall until it times
        # out on the server side. End the session to force the server
        # to close the transaction.
        kill_all_server_sessions
      end
    end

    context 'when a Mongo error is raised' do
      before do
        collection.insert_one({foo: 1})
      end

      it 'swallows the exception and sets state to transaction aborted' do
        session.start_transaction
        collection.insert_one({foo: 1}, session: session)
        expect(session).to receive(:write_with_retry).and_raise(Mongo::Error::SocketError)
        expect do
          session.abort_transaction
        end.not_to raise_error
        expect(session.send(:within_states?, Mongo::Session::TRANSACTION_ABORTED_STATE)).to be true

        # Since we failed abort_transaction call, the transaction is still
        # outstanding. It will cause subsequent tests to stall until it times
        # out on the server side. End the session to force the server
        # to close the transaction.
        kill_all_server_sessions
      end
    end
  end

  describe '#with_transaction' do
    require_topology :replica_set

    context 'callback successful' do
      it 'commits' do
        session.with_transaction do
          collection.insert_one(a: 1)
        end

        result = collection.find(a: 1).first
        expect(result[:a]).to eq(1)
      end

      it 'propagates callback\'s return value' do
        rv = session.with_transaction do
          42
        end
        expect(rv).to eq(42)
      end
    end

    context 'callback raises' do
      it 'propagates the exception' do
        expect do
          session.with_transaction do
            raise SessionTransactionSpecError, 'test error'
          end
        end.to raise_error(SessionTransactionSpecError, 'test error')
      end
    end

    context 'callback aborts transaction' do
      it 'does not raise exceptions and propagates callback\'s return value' do
        rv = session.with_transaction do
          session.abort_transaction
          42
        end
        expect(rv).to eq(42)
      end
    end

    context 'timeout with callback raising TransientTransactionError' do
      max_example_run_time 7

      it 'times out' do
        start = Mongo::Utils.monotonic_time

        expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start)
        expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 1)
        expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 2)
        expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 200)
        allow(session).to receive('check_transactions_supported!').and_return true

        expect do
          session.with_transaction do
            exc = Mongo::Error::OperationFailure.new('timeout test')
            exc.add_label('TransientTransactionError')
            raise exc
          end
        end.to raise_error(Mongo::Error::OperationFailure, 'timeout test')
      end
    end

    %w(UnknownTransactionCommitResult TransientTransactionError).each do |label|
      context "timeout with commit raising with #{label}" do
        max_example_run_time 7

        # JRuby seems to burn through the monotonic time expectations
        # very quickly and the retries of the transaction get the original
        # time which causes the transaction to be stuck there.
        fails_on_jruby

        before do
          # create collection if it does not exist
          collection.insert_one(a: 1)
        end

        retry_test
        it 'times out' do
          start = Mongo::Utils.monotonic_time

          11.times do |i|
            expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + i)
          end
          expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 200)
          allow(session).to receive('check_transactions_supported!').and_return true

          exc = Mongo::Error::OperationFailure.new('timeout test')
          exc.add_label(label)

          expect(session).to receive(:commit_transaction).and_raise(exc).at_least(:once)

          expect do
            session.with_transaction do
              collection.insert_one(a: 2)
            end
          end.to raise_error(Mongo::Error::OperationFailure, 'timeout test')
        end
      end
    end

    context 'callback breaks out of with_tx loop' do
      it 'aborts transaction' do
        expect(session).to receive(:start_transaction).and_call_original
        expect(session).to receive(:abort_transaction).and_call_original
        expect(session).to receive(:log_warn).and_call_original

        session.with_transaction do
          break
        end
      end
    end

    context 'application timeout around with_tx' do
      it 'keeps session in a working state' do
        session
        collection.insert_one(a: 1)

        expect do
          Timeout.timeout(1, SessionTransactionSpecError) do
            session.with_transaction do
              sleep 2
            end
          end
        end.to raise_error(SessionTransactionSpecError)

        session.with_transaction do
          collection.insert_one(timeout_around_with_tx: 2)
        end

        expect(collection.find(timeout_around_with_tx: 2).first).not_to be nil
      end
    end
  end
end
