require 'vcr/util/logger'
require 'vcr/cassette/http_interaction_list'
require 'vcr/request_matcher_registry'
require 'vcr/structs'
require 'support/configuration_stubbing'

module VCR
  class Cassette
    ::RSpec.describe HTTPInteractionList do
      include_context "configuration stubbing"

      ::RSpec::Matchers.define :respond_with do |expected|
        match { |a| expected.nil? ? a.nil? : a.body == expected }
      end

      before(:each) do
        allow(VCR).to receive(:request_matchers).and_return(VCR::RequestMatcherRegistry.new)
        allow(config).to receive(:logger).and_return(double.as_null_object)
      end

      def request_with(values)
        VCR::Request.new.tap do |request|
          values.each do |name, value|
            request.send("#{name}=", value)
          end
        end
      end

      def response(body)
        VCR::Response.new.tap do |r|
          r.body = body
          r.status = VCR::ResponseStatus.new(200)
        end
      end

      def interaction(body, request_values)
        VCR::HTTPInteraction.new \
          request_with(request_values),
          response(body)
      end

      let(:original_list_array) do [
        interaction('put response', :method => :put),
        interaction('post response 1', :method => :post),
        interaction('post response 2', :method => :post)
      ] end

      let(:allow_playback_repeats) { false } # the default
      let(:list) { HTTPInteractionList.new(original_list_array, [:method], allow_playback_repeats) }

      describe "#has_used_interaction_matching?" do
        it 'returns false when no interactions have been used' do
          expect(list).not_to have_used_interaction_matching(request_with(:method => :put))
        end

        it 'returns true when there is a matching used interaction (even if there is also an unused one that matches)' do
          list.response_for(request_with(:method => :post))
          expect(list).to have_used_interaction_matching(request_with(:method => :post))
        end

        it 'returns false when none of the used interactions match' do
          list.response_for(request_with(:method => :put))
          expect(list).not_to have_used_interaction_matching(request_with(:method => :post))
        end
      end

      describe "#remaining_unused_interaction_count" do
        it 'returns the number of unused interactions' do
          expect(list.remaining_unused_interaction_count).to eq(3)

          list.response_for(request_with(:method => :get))
          expect(list.remaining_unused_interaction_count).to eq(3)

          list.response_for(request_with(:method => :put))
          expect(list.remaining_unused_interaction_count).to eq(2)

          list.response_for(request_with(:method => :put))
          expect(list.remaining_unused_interaction_count).to eq(2)

          list.response_for(request_with(:method => :post))
          expect(list.remaining_unused_interaction_count).to eq(1)

          list.response_for(request_with(:method => :post))
          expect(list.remaining_unused_interaction_count).to eq(0)

          list.response_for(request_with(:method => :post))
          expect(list.remaining_unused_interaction_count).to eq(0)
        end
      end

      describe "#assert_no_unused_interactions?" do
        it 'should raise a SkippedHTTPRequestError when there are unused interactions left' do
          expect {
            list.assert_no_unused_interactions!
          }.to raise_error(Errors::UnusedHTTPInteractionError)

          list.response_for(request_with(:method => :put))
          expect {
            list.assert_no_unused_interactions!
          }.to raise_error(Errors::UnusedHTTPInteractionError)
        end

        it 'should raise nothing when there are no unused interactions left' do
          [:put, :post, :post].each do |method|
            list.response_for(request_with(:method => method))
          end

          expect {
            list.assert_no_unused_interactions!
          }.not_to raise_error
        end

        context 'when the null logger is in use' do
          before { allow(config).to receive(:logger).and_return(Logger::Null) }

          it 'includes formatted request details in the error message' do
            expect {
              list.assert_no_unused_interactions!
            }.to raise_error(/\[put/)
          end

          it 'includes formatted response details in the error message' do
            expect {
              list.assert_no_unused_interactions!
            }.to raise_error(/\[200 "put response"\]/)
          end
        end
      end

      describe "has_interaction_matching?" do
        it 'returns false when the list is empty' do
          expect(HTTPInteractionList.new([], [:method])).not_to have_interaction_matching(double)
        end

        it 'returns false when there is no matching interaction' do
          expect(list).not_to have_interaction_matching(request_with(:method => :get))
        end

        it 'returns true when there is a matching interaction' do
          expect(list).to have_interaction_matching(request_with(:method => :post))
        end

        it 'does not consume the interactions when they match' do
          expect(list).to have_interaction_matching(request_with(:method => :post))
          expect(list.remaining_unused_interaction_count).to eq(3)
          expect(list).to have_interaction_matching(request_with(:method => :post))
          expect(list.remaining_unused_interaction_count).to eq(3)
        end

        it 'invokes each matcher block to find the matching interaction' do
          VCR.request_matchers.register(:foo) { |r1, r2| true }
          VCR.request_matchers.register(:bar) { |r1, r2| true }

          calls = 0
          VCR.request_matchers.register(:baz) { |r1, r2| calls += 1; calls == 2 }

          list = HTTPInteractionList.new([
            interaction('response', :method => :put)
          ], [:foo, :bar, :baz])

          expect(list).not_to have_interaction_matching(request_with(:method => :post))
          expect(list).to     have_interaction_matching(request_with(:method => :post))
        end

        it "delegates to the parent list when it can't find a matching interaction" do
          parent_list = double(:has_interaction_matching? => true)
          expect(HTTPInteractionList.new( [], [:method], false, parent_list)).to have_interaction_matching(double)
          parent_list = double(:has_interaction_matching? => false)
          expect(HTTPInteractionList.new( [], [:method], false, parent_list)).not_to have_interaction_matching(double)
        end

        context 'when allow_playback_repeats is set to true' do
          let(:allow_playback_repeats) { true }

          it 'considers used interactions' do
            list.response_for(request_with(:method => :put))

            results = 10.times.map do
              list.has_interaction_matching?(request_with(:method => :put))
            end

            expect(results).to eq([true] * 10)
          end
        end

        context 'when allow_playback_repeats is set to false' do
          let(:allow_playback_repeats) { false }

          it 'does not consider used interactions' do
            list.response_for(request_with(:method => :put))

            result = 10.times.map do
              list.has_interaction_matching?(request_with(:method => :put))
            end

            expect(result).to eq([false] * 10)
          end
        end
      end

      describe "#response_for" do
        it 'returns nil when the list is empty' do
          expect(HTTPInteractionList.new([], [:method]).response_for(double)).to respond_with(nil)
        end

        it 'returns nil when there is no matching interaction' do
          response = HTTPInteractionList.new([
            interaction('foo', :method => :post),
            interaction('foo', :method => :put)
          ], [:method]).response_for(
            request_with(:method => :get)
          )

          expect(response).to respond_with(nil)
        end

        it 'returns the first matching interaction' do
          list = HTTPInteractionList.new([
            interaction('put response', :method => :put),
            interaction('post response 1', :method => :post),
            interaction('post response 2', :method => :post)
          ], [:method])

          expect(list.response_for(request_with(:method => :post))).to respond_with("post response 1")
        end

        it 'invokes each matcher block to find the matching interaction' do
          VCR.request_matchers.register(:foo) { |r1, r2| true }
          VCR.request_matchers.register(:bar) { |r1, r2| true }

          calls = 0
          VCR.request_matchers.register(:baz) { |r1, r2| calls += 1; calls == 2 }

          list = HTTPInteractionList.new([
            interaction('response', :method => :put)
          ], [:foo, :bar, :baz])

          expect(list.response_for(request_with(:method => :post))).to respond_with(nil)
          expect(list.response_for(request_with(:method => :post))).to respond_with('response')
        end

        it "delegates to the parent list when it can't find a matching interaction" do
          parent_list = double(:response_for => response('parent'))
          result = HTTPInteractionList.new(
            [], [:method], false, parent_list
          ).response_for(double)

          expect(result).to respond_with('parent')
        end

        it 'consumes the first matching interaction so that it will not be used again' do
          expect(list.response_for(request_with(:method => :post)).body).to eq("post response 1")
          expect(list.response_for(request_with(:method => :post)).body).to eq("post response 2")
        end

        context 'when allow_playback_repeats is set to true' do
          let(:allow_playback_repeats) { true }

          it 'continues to return the response from the last matching interaction when there are no more' do
            list.response_for(request_with(:method => :post))

            results = 10.times.map do
              response = list.response_for(request_with(:method => :post))
              response ? response.body : nil
            end

            expect(results).to eq(["post response 2"] * 10)
          end
        end

        context 'when allow_playback_repeats is set to false' do
          let(:allow_playback_repeats) { false }

          it 'returns nil when there are no more unused interactions' do
            list.response_for(request_with(:method => :post))
            list.response_for(request_with(:method => :post))

            results = 10.times.map do
              list.response_for(request_with(:method => :post))
            end

            expect(results).to eq([nil] * 10)
          end
        end

        it 'does not modify the original interaction array the list was initialized with' do
          original_dup = original_list_array.dup
          list.response_for(request_with(:method => :post))
          expect(original_list_array).to eq original_dup
        end
      end
    end
  end
end

