require 'kontena_cli'
require 'ostruct'

describe Kontena::Client do

  let(:subject) { described_class.new('https://localhost/v1/') }
  let(:http_client) { double(:http_client) }

  # This trickery is here for making the tests work with or without the new configuration handler.
  # The client itself will work with any kind of token that acts like a hash or ostruct.
  let(:server_class)   { Kontena::Cli.const_defined?('Config', false) ? Kontena::Cli::Config::Server  : OpenStruct }
  let(:token_class)    { Kontena::Cli.const_defined?('Config', false) ? Kontena::Cli::Config::Token   : OpenStruct }
  let(:account_class)  { Kontena::Cli.const_defined?('Config', false) ? Kontena::Cli::Config::Account : OpenStruct }

  let(:master) { server_class.new(url: 'https://localhost', name: 'master') }
  let(:token) { token_class.new(access_token: '1234', refresh_token: '5678', expires_at: nil, parent_type: :master, parent_name: 'master') }
  let(:expiring_token) { token_class.new(access_token: '1234', refresh_token: '5678', expires_at: Time.now.utc + 1000, parent_type: :master, parent_name: 'master') }
  let(:expired_token) { token_class.new(access_token: '1234', refresh_token: '5678', expires_at: Time.now.utc - 1000, parent_type: :master, parent_name: 'master') }

  before(:each) do
    allow(subject).to receive(:http_client).and_return(http_client)
    if Kontena::Cli.const_defined?('Config', false)
      config = Kontena::Cli::Config
    else
      config = Class.new { include Kontena::Cli::Common}.new
    end
    config.servers << master
    allow(config).to receive(:find_server).and_return(master)
    allow(config).to receive(:current_master).and_return(master)
    allow(config).to receive(:account).and_return(account_class.new(token_verify_path: '/v1/user', token_endpoint: '/oauth2/token', authorization_endpoint: '/oauth2/authorize'))
    allow(config).to receive(:write).and_return(true)
  end

  context 'token authentication' do

    it 'takes a token' do
      client = Kontena::Client.new('https://localhost/v1/', token)
      expect(client.token).to eq token
    end

    it 'uses the access token as a bearer token' do
      client = Kontena::Client.new('https://localhost/v1/', token)
      expect(client.http_client).to receive(:request) do |opts|
        expect(opts[:headers]['Authorization']).to eq "Bearer #{token.access_token}"
      end.and_return(spy(:response, status: 200))
      client.get('/v1/foo')
    end

    it 'does not try to refresh an expiring token that is still valid' do
      client = Kontena::Client.new('https://localhost/v1/', expiring_token)
      expect(client.http_client).to receive(:request) do |opts|
        expect(opts[:headers]['Authorization']).to eq "Bearer #{token.access_token}"
      end.and_return(spy(:response, status: 200))
      client.get('/v1/foo')
    end

    it 'tries to refresh an expired token' do
      master.token = expired_token
      client = Kontena::Client.new(master.url, master.token)
      allow(client).to receive(:token_refresh_path).and_return('/oauth2/token')
      expect(client.http_client).to receive(:request).with(hash_including(path: '/oauth2/token', method: :post)).and_return(OpenStruct.new(status: 201, headers: {'Content-Type' => 'application/json'}, body: '{"access_token": "abcd"}'))
      expect(client.http_client).to receive(:request).with(hash_including(path: '/v1/foo')) do |args|
        expect(args[:headers]['Authorization']).to eq "Bearer abcd"
      end.and_return(spy(:response, status: 200))
      client.get('/v1/foo')
    end
  end

  describe '#get' do
    it 'passes path to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(path: '/v1/foo', method: :get)
      ).and_return(spy(:response, status: 200))
      subject.get('foo')
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(query: {bar: 'baz'}, method: :get)
      ).and_return(spy(:response, status: 200))
      subject.get('foo', {bar: 'baz'})
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(headers: hash_including('Some-Header' => 'value'), method: :get)
      ).and_return(spy(:response, status: 200))
      subject.get('foo', nil, {'Some-Header' => 'value'})
    end
  end

  describe '#get_stream' do
    let(:response_block) { Proc.new{ } }

    it 'passes path & response_block to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(path: '/v1/foo', response_block: response_block, method: :get)
      ).and_return(spy(:response, status: 200))
      subject.get_stream('foo', response_block)
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(query: {bar: 'baz'}, method: :get)
      ).and_return(spy(:response, status: 200))
      subject.get_stream('foo', response_block, {bar: 'baz'})
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(headers: hash_including('Some-Header' => 'value'), method: :get)
      ).and_return(spy(:response, status: 200))
      subject.get_stream('foo', response_block, nil, {'Some-Header' => 'value'})
    end
  end

  describe '#post' do
    let(:data) do
      { foo: 'bar' }
    end

    it 'passes path and object to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(path: '/v1/foo', body: kind_of(String), method: :post)
      ).and_return(spy(:response, status: 200))
      subject.post('foo', data)
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(query: {bar: 'baz'}, method: :post)
      ).and_return(spy(:response, status: 200))
      subject.post('foo', data, {bar: 'baz'})
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(headers: hash_including('Some-Header' => 'value'), method: :post)
      ).and_return(spy(:response, status: 200))
      subject.post('foo', data, nil, {'Some-Header' => 'value'})
    end
  end

  describe '#put' do
    let(:data) do
      { foo: 'bar' }
    end

    it 'passes path and object to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(path: '/v1/foo', body: kind_of(String), method: :put)
      ).and_return(spy(:response, status: 200))
      subject.put('foo', data)
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(query: {bar: 'baz'}, method: :put)
      ).and_return(spy(:response, status: 200))
      subject.put('foo', data, {bar: 'baz'})
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(headers: hash_including('Some-Header' => 'value'), method: :put)
      ).and_return(spy(:response, status: 200))
      subject.put('foo', data, nil, {'Some-Header' => 'value'})
    end
  end

  describe '#delete' do
    let(:data) do
      { foo: 'bar' }
    end

    it 'passes path to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(path: '/v1/foo', method: :delete)
      ).and_return(spy(:response, status: 200))
      subject.delete('foo')
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(query: {bar: 'baz'}, method: :delete)
      ).and_return(spy(:response, status: 200))
      subject.delete('foo', nil, {bar: 'baz'})
    end

    it 'passes params to client' do
      allow(subject).to receive(:http_client).and_return(http_client)
      expect(http_client).to receive(:request).with(
        hash_including(headers: hash_including('Some-Header' => 'value'), method: :delete)
      ).and_return(spy(:response, status: 200))
      subject.delete('foo', nil, nil, {'Some-Header' => 'value'})
    end
  end

  describe '#request' do
    subject do
      Kontena::Client.new('http://localhost', master.token)
    end

    context "for an expected response" do
      before :each do
        allow(subject).to receive(:http_client).and_call_original

        WebMock.stub_request(:any, 'http://localhost/v1/test').to_return(
        status: 200,
        headers: {
          'Content-Type' => 'application/json',
        },
        body: {'test' => [ "This was a triumph.", "I’m making a note here: HUGE SUCCESS." ]}.to_json,
      )
      end

      it "returns the JSON object" do
        expect(subject.get('test')['test'].join(" / ")).to eq "This was a triumph. / I’m making a note here: HUGE SUCCESS."
      end
    end

    context "with empty response JSON" do
      before :each do
        allow(subject).to receive(:http_client).and_call_original

        WebMock.stub_request(:delete, 'http://localhost/v1/test').to_return(
          status: 200,
          headers: {
            'Content-Type' => 'application/json',
          },
          body: '',
        )
      end

      it "returns nil" do
        expect(subject.delete('test')).to be_nil
      end
    end

    context "with invalid response JSON" do
      before :each do
        allow(subject).to receive(:http_client).and_call_original

        WebMock.stub_request(:any, 'http://localhost/v1/test').to_return(
          status: 200,
          headers: {
            'Content-Type' => 'application/json',
          },
          body: 'this is not valid JSON',
        )
      end

      it "raises Kontena::Errors::StandardError with 520" do
        # XXX: for some reason the response.path is empty?
        expect{subject.get('test')}.to raise_error(Kontena::Errors::StandardError, /Invalid response JSON from server for : JSON::ParserError/)
      end
    end

    context "with an empty error response" do
      before :each do
        # workaround https://github.com/bblimke/webmock/issues/653
        expect(http_client).to receive(:request).with(
          hash_including(path: '/v1/coffee', method: :brew)
        ) {
          raise Excon::Error::HTTPStatus.new("I'm a teapot",
            {
              method: 'brew',
              path: '/v1/coffee',
            },
            double(:response,
              status: 418,
              path: '/foo',
              reason_phrase: "I'm a teapot",
              headers: {
                'Content-Type' => 'short/stout',
              },
              body: "",
            )
          )
        }
      end

      it "raises StandardError with the status phrase" do
        expect{subject.request(http_method: :brew, path: 'coffee')}.to raise_error(Kontena::Errors::StandardError, /I'm a teapot/)
      end
    end

    context "with an 500 response with text error" do
      before :each do
        allow(subject).to receive(:http_client).and_call_original

        WebMock.stub_request(:any, 'http://localhost/v1/print').to_return(
          status: 500,
          body: "lp0 (printer) on fire",
        )
      end

      it "raises StandardError with the server error message" do
        expect{subject.post('print', { 'code' => "8A/HyA==" })}.to raise_error(Kontena::Errors::StandardError, /lp0 \(printer\) on fire/)
      end
    end

    context "with a 422 response with JSON error string" do
      before :each do
        allow(subject).to receive(:http_client).and_call_original

        WebMock.stub_request(:any, 'http://localhost/v1/test').to_return(
          status: 422,
          headers: {
            'Content-Type' => 'application/json',
          },
          body: {'error' => "You are wrong"}.to_json,
        )
      end

      it "raises StandardError with the server error message" do
        expect{subject.get('test')}.to raise_error(Kontena::Errors::StandardError, /You are wrong/)
      end
    end

    context "with a 422 response with JSON error object" do
      before :each do
        allow(subject).to receive(:http_client).and_call_original

        WebMock.stub_request(:any, 'http://localhost/v1/test').to_return(
          status: 422,
          headers: {
            'Content-Type' => 'application/json',
          },
          body: {'error' => { 'foo' => "Foo was invalid" } }.to_json,
        )
      end

      it "raises StandardError with the server error message" do
        expect{subject.get('test')}.to raise_error(Kontena::Errors::StandardErrorHash, /foo: Foo was invalid/)
      end
    end

    context 'version warning' do
      let(:client) { double(:client) }
      let(:response) { double(:response) }
      let(:cli_version) { Kontena::Cli::VERSION }

      before(:each) do
        allow(subject).to receive(:http_client).and_return(client)
        allow(client).to receive(:request).and_return(response)
        allow(response).to receive(:body).and_return("hello")
        allow(response).to receive(:headers).and_return({})
      end

      it 'warns the user once if server version differs enough from master version' do
        bumped_version = cli_version.split('.')[0] + '.' + (cli_version.split('.')[1].to_i + 1).to_s + '.0' # 1.5.4 --> 1.6.0
        expect(response).to receive(:headers).at_least(:once).and_return({'X-Kontena-Version' => bumped_version})
        expect(subject).to receive(:check_version_and_warn).at_least(:once).and_call_original
        expect(subject).to receive(:add_version_warning).with(bumped_version).once.and_return(true)
        expect(subject.get("test")).to eq 'hello'
        expect(subject.get("test")).to eq 'hello'
      end

      it 'does not warn the user if server version does not differ too much' do
        bumped_version = cli_version.split('.')[0] + '.' + cli_version.split('.')[1] + '.' + (cli_version.split('.')[2].to_i + 1).to_s # 1.5.4 --> 1.5.5
        expect(response).to receive(:headers).at_least(:once).and_return({'X-Kontena-Version' => bumped_version})
        expect(subject).to receive(:check_version_and_warn).at_least(:once).and_call_original
        expect(subject).not_to receive(:at_exit)
        expect(subject).not_to receive(:add_version_warning)
        expect(subject.get("test")).to eq 'hello'
      end

      it 'does not warn the user if server version is not there at all' do
        expect(response).to receive(:headers).at_least(:once).and_return({})
        allow(subject).to receive(:check_version_and_warn).at_least(:once).and_call_original
        expect(subject).not_to receive(:at_exit)
        expect(subject).not_to receive(:add_version_warning)
        expect(subject.get("test")).to eq 'hello'
      end
    end
  end
end
