require 'spec_helper'
require 'request_spec_shared_examples'
require 'cloud_controller'
require 'services'
require 'messages/service_broker_update_message'

# The request specs test single actions.
# The lifecycle tests are about combinations of actions
RSpec.describe 'V3 service brokers' do
  let(:catalog) do
    {
      'services' => [
        {
          'id' => 'catalog1',
          'name' => 'service_name-1',
          'description' => 'some description 1',
          'bindable' => true,
          'plans' => [
            {
              'id' => 'fake_plan_id-1',
              'name' => 'plan_name-1',
              'description' => 'fake_plan_description 1',
              'schemas' => nil
            }
          ]
        },
        {
          'id' => 'catalog2',
          'name' => 'route_volume_service_name-2',
          'requires' => %w[volume_mount route_forwarding],
          'description' => 'some description 2',
          'bindable' => true,
          'plans' => [
            {
              'id' => 'fake_plan_id-2',
              'name' => 'plan_name-2',
              'description' => 'fake_plan_description 2',
              'schemas' => nil
            }
          ]
        }
      ]
    }.to_json
  end

  describe 'POST /v3/service_brokers' do
    let(:create_request_body) do
      {
        name: 'my-service-broker',
        url: 'http://example.org/my-service-broker-url',
        authentication: {
          type: 'basic',
          credentials: {
            username: 'admin',
            password: 'password'
          }
        },
        metadata: {
          labels: { to_update: 'value', to_delete: 'value', 'to.delete/with_prefix' => 'value' },
          annotations: { to_update: 'value', to_delete: 'value', 'to.delete/with_prefix' => 'value' }
        }
      }
    end

    let(:job_url_for_create) do
      post '/v3/service_brokers', create_request_body.to_json, admin_headers
      expect(last_response).to have_status_code(202)
      last_response['Location']
    end

    describe 'successful creation' do
      before do
        stub_request(:get, 'http://example.org/my-service-broker-url/v2/catalog').
          with(basic_auth: %w[admin password]).
          to_return(status: 200, body: catalog, headers: {})
      end

      it 'creates a service broker' do
        # Request new broker
        get job_url_for_create, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response['state']).to eq('PROCESSING')
        broker = parsed_response.dig('links', 'service_brokers', 'href')

        # Check it finishes
        execute_all_jobs(expected_successes: 1, expected_failures: 0)
        get job_url_for_create, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response['state']).to eq('COMPLETE')

        # Check it's correct
        get broker, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to include(
          'name' => 'my-service-broker',
          'url' => 'http://example.org/my-service-broker-url'
        )
      end
    end

    describe 'failed creation' do
      before do
        stub_request(:get, 'http://example.org/my-service-broker-url/v2/catalog').
          with(basic_auth: %w[admin password]).
          to_return(status: 404)
      end

      it 'creates a service broker in failed state' do
        # Request new broker
        get job_url_for_create, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response['state']).to eq('PROCESSING')

        # Check it finishes
        execute_all_jobs(expected_successes: 0, expected_failures: 1)
        get job_url_for_create, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response['state']).to eq('FAILED')
      end
    end

    context 'while a broker is being created' do
      describe 'creation during creation' do
        before do
          stub_request(:get, 'http://example.org/my-service-broker-url/v2/catalog').
            with(basic_auth: %w[admin password]).
            to_return(status: 200, body: catalog, headers: {})
        end

        it 'rejects a duplicate name' do
          post '/v3/service_brokers', create_request_body.to_json, admin_headers
          expect(last_response).to have_status_code(202)

          post '/v3/service_brokers', create_request_body.to_json, admin_headers
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors'][0]['detail']).to match('Name must be unique')
        end

        it 'allows a different name' do
          post '/v3/service_brokers', create_request_body.to_json, admin_headers
          expect(last_response).to have_status_code(202)

          other_request_body = create_request_body.merge({ 'name' => 'my-other-broker' }).to_json
          post '/v3/service_brokers', other_request_body, admin_headers
          expect(last_response).to have_status_code(202)
        end
      end

      describe 'deletion during creation' do
        before do
          stub_request(:get, 'http://example.org/my-service-broker-url/v2/catalog').
            with(basic_auth: %w[admin password]).
            to_return(status: 200, body: catalog, headers: {})
        end

        it 'allows deletion during creation' do
          # Request new broker
          get job_url_for_create, {}, admin_headers
          expect(last_response).to have_status_code(200)
          expect(parsed_response['state']).to eq('PROCESSING')
          broker = parsed_response.dig('links', 'service_brokers', 'href')

          # Delete it
          delete broker, {}, admin_headers
          expect(last_response).to have_status_code(202)
          get last_response['Location'], {}, admin_headers
          expect(last_response).to have_status_code(200)
          expect(parsed_response['state']).to eq('PROCESSING')

          # Check it's not there
          execute_all_jobs(expected_successes: 2, expected_failures: 0)
          get broker, {}, admin_headers
          expect(last_response).to have_status_code(404)
        end
      end

      describe 'update during creation' do
        before do
          stub_request(:get, 'http://example.org/my-service-broker-url/v2/catalog').
            with(basic_auth: %w[admin password]).
            to_return(status: 200, body: catalog, headers: {})
        end

        it 'blocks update during creation' do
          # Request new broker
          get job_url_for_create, {}, admin_headers
          expect(last_response).to have_status_code(200)
          expect(parsed_response['state']).to eq('PROCESSING')
          broker = parsed_response.dig('links', 'service_brokers', 'href')

          # Patch should be blocked
          patch broker, create_request_body.to_json, admin_headers
          expect(last_response).to have_status_code(422)
          expect(parsed_response['errors'][0]['detail']).to match('Cannot update a broker when other operation is already in progress')
        end
      end
    end
  end

  describe 'PATCH /v3/service_brokers/:guid' do
    let(:create_request_body) do
      {
        name: 'old-name',
        url: 'http://example.org/old-broker-url',
        authentication: {
          type: 'basic',
          credentials: {
            username: 'old-admin',
            password: 'not-welcome'
          }
        },
        metadata: {
          labels: { to_update: 'value', to_delete: 'value', 'to.delete/with_prefix' => 'value' },
          annotations: { to_update: 'value', to_delete: 'value', 'to.delete/with_prefix' => 'value' }
        }
      }
    end

    let(:update_request_body) do
      {
        name: 'new-name',
        url: 'http://example.org/new-broker-url',
        authentication: {
          type: 'basic',
          credentials: {
            username: 'admin',
            password: 'welcome'
          }
        },
        metadata: {
          labels: { to_update: 'changed-value', to_delete: nil, to_add: 'new-value', 'to.delete/with_prefix' => nil },
          annotations: { to_update: 'changed-value', to_delete: nil, to_add: 'new-value', 'to.delete/with_prefix' => nil }
        }
      }
    end

    let(:broker) { create_service_broker }

    context 'is successful' do
      before do
        stub_request(:get, 'http://example.org/new-broker-url/v2/catalog').
          with(basic_auth: %w[admin welcome]).
          to_return(status: 200, body: catalog, headers: {})

        patch "/v3/service_brokers/#{broker['guid']}", update_request_body.to_json, admin_headers
        expect(last_response).to have_status_code(202)

        job_url = last_response['Location']
        get job_url, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to include({
                                             'state' => 'PROCESSING',
                                             'operation' => 'service_broker.update',
                                             'errors' => [],
                                             'warnings' => []
                                           })

        execute_all_jobs(expected_successes: 1, expected_failures: 0)

        get job_url, {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to include({
                                             'state' => 'COMPLETE',
                                             'operation' => 'service_broker.update',
                                             'errors' => [],
                                             'warnings' => []
                                           })
      end

      it 'successfully updates the service broker' do
        get "/v3/service_brokers/#{broker['guid']}", {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to include({
                                             'name' => 'new-name',
                                             'url' => 'http://example.org/new-broker-url'
                                           })
      end

      it 'adds, removes and updates metadata when the request contains metadata changes' do
        get "/v3/service_brokers/#{broker['guid']}", {}, admin_headers
        expect(last_response).to have_status_code(200)
        expect(parsed_response).to include({
                                             'metadata' => {
                                               'labels' => { 'to_update' => 'changed-value', 'to_add' => 'new-value' },
                                               'annotations' => { 'to_update' => 'changed-value', 'to_add' => 'new-value' }
                                             }
                                           })
      end
    end

    it 'fails to update the service broker' do
      stub_request(:get, 'http://example.org/new-broker-url/v2/catalog').
        with(basic_auth: %w[admin welcome]).
        to_return(status: 500, body: '', headers: {})

      patch "/v3/service_brokers/#{broker['guid']}", update_request_body.to_json, admin_headers
      expect(last_response).to have_status_code(202)

      execute_all_jobs(expected_successes: 0, expected_failures: 1)

      job_url = last_response['Location']
      get job_url, {}, admin_headers
      expect(last_response).to have_status_code(200)
      expect(parsed_response).to include({
                                           'state' => 'FAILED',
                                           'operation' => 'service_broker.update',
                                           'errors' => [include({ 'code' => 10_001, 'detail' => include('The service broker returned an invalid response') })],
                                           'warnings' => []
                                         })

      get "/v3/service_brokers/#{broker['guid']}", {}, admin_headers
      expect(last_response).to have_status_code(200)
      expect(parsed_response).to include({
                                           'name' => 'old-name',
                                           'url' => 'http://example.org/old-broker-url',
                                           'metadata' => {
                                             'annotations' => {
                                               'to_delete' => 'value',
                                               'to_update' => 'value',
                                               'to.delete/with_prefix' => 'value'
                                             },
                                             'labels' => {
                                               'to_delete' => 'value',
                                               'to_update' => 'value',
                                               'to.delete/with_prefix' => 'value'
                                             }
                                           }
                                         })
    end

    %i[delete patch].each do |http_method|
      it "errors when a #{http_method} is in progress" do
        method(http_method).call "/v3/service_brokers/#{broker['guid']}", update_request_body.to_json, admin_headers
        expect(last_response).to have_status_code(202)

        patch "/v3/service_brokers/#{broker['guid']}", update_request_body.to_json, admin_headers
        expect(last_response).to have_status_code(422)
        expect(parsed_response).to include(
          'errors' => [include({
                                 'code' => 10_008,
                                 'detail' => include('Cannot update a broker when other operation is already in progress')
                               })]
        )
        expect(last_response['Location']).to be_nil
      end
    end

    it 'errors when the broker creation is still in progress' do
      post '/v3/service_brokers', create_request_body.to_json, admin_headers
      expect(last_response).to have_status_code(202)

      broker = broker_response_from_job(last_response['Location'])

      patch "/v3/service_brokers/#{broker['guid']}", update_request_body.to_json, admin_headers
      expect(last_response).to have_status_code(422)
      expect(parsed_response).to include(
        'errors' => [include({
                               'code' => 10_008,
                               'detail' => include('Cannot update a broker when other operation is already in progress')
                             })]
      )
      expect(last_response['Location']).to be_nil
    end
  end

  def broker_response_from_job(job_url)
    get job_url, {}, admin_headers
    expect(last_response).to have_status_code(200)

    get parsed_response.dig('links', 'service_brokers', 'href'), {}, admin_headers
    expect(last_response).to have_status_code(200)

    parsed_response
  end

  def create_service_broker
    stub_request(:get, 'http://example.org/old-broker-url/v2/catalog').
      with(basic_auth: %w[old-admin not-welcome]).
      to_return(status: 200, body: catalog, headers: {})

    post '/v3/service_brokers', create_request_body.to_json, admin_headers
    expect(last_response).to have_status_code(202)

    execute_all_jobs(expected_successes: 1, expected_failures: 0)

    broker_response_from_job(last_response['Location'])
  end
end
