require_relative "../../../spec_helper"
require 'kontena/cli/master/login_command'
require 'kontena/cli/localhost_web_server'
require 'launchy'
require 'ostruct'

describe Kontena::Cli::Master::LoginCommand do

  include ClientHelpers

  let(:subject) do
    described_class.new(File.basename($0))
  end

  let(:client) { double(:client) }

  it 'should exit with error if --code and --token both given' do
    expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
    subject.run(%w(--code abcd --token defg))
  end

  it 'should exit with error if --code and --join both given' do
    expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
    subject.run(%w(--code abcd --join defg))
  end

  it 'should exit with error if --code and --force both given' do
    expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
    subject.run(%w(--code abcd --force))
  end

  it 'should exit with error if --token and --force both given' do
    expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
    subject.run(%w(--token abcd --force))
  end

  describe '#select_a_server' do
    let(:config) { double(:config) }
    let(:server) { Kontena::Cli::Config::Server.new(url: 'https://foo', name: 'server') }

    before(:each) do
      allow(subject).to receive(:config).and_return(config)
    end

    context 'no url or name provided' do
      it 'tries to use current_master' do
        expect(config).to receive(:current_master).at_least(:once).and_return(server)
        expect(subject.select_a_server(nil, nil)).to eq server
      end

      it 'exits with error if current_master not set' do
        expect(config).to receive(:current_master).and_return(nil)
        expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
        subject.select_a_server(nil, nil)
      end
    end

    context 'name provided' do
      context 'with url' do
        it 'should return the existing server if an exact match is found' do
          expect(config).to receive(:find_server_by).and_return(server)
          expect(subject.select_a_server(server.name, server.url)).to be server
        end

        it 'should return the existing one but update its url if a name match is found' do
          allow(config).to receive(:find_server_by).and_return(nil)
          expect(config).to receive(:find_server).and_return(server)
          expect(subject.select_a_server(server.name, 'http://foofoo')).to be server
          expect(server.url).to eq 'http://foofoo'
        end

        it 'should create a new server entry if no exact match or name match is found' do
          allow(config).to receive(:find_server_by).and_return(nil)
          allow(config).to receive(:find_server).and_return(nil)
          new_server = subject.select_a_server('fooserver', 'http://foofoo')
          expect(new_server).not_to be server
          expect(new_server.url).to eq 'http://foofoo'
          expect(new_server.name).to eq 'fooserver'
        end
      end

      context 'without url' do
        it 'should return a server with that name if it has an url' do
          expect(config).to receive(:find_server).with('foo').and_return(server)
          expect(subject.select_a_server('foo', nil)).to be server
          expect(server.url).not_to be_nil
        end

        it 'should exit with error if a server with that name is found but it does not have an url' do
          server.url = nil
          expect(config).to receive(:find_server).with('foo').and_return(server)
          expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
          subject.select_a_server('foo', nil)
        end
      end
    end

    context 'url provided without name' do
      context 'url looks like an url' do
        it 'should try to find a server with that url and return it if found' do
          expect(config).to receive(:find_server_by).with({ :url => 'https://foo' }).and_return(server)
          expect(subject.select_a_server(nil, 'https://foo')).to be server
        end

        it 'should create a new server entry if not found' do
          expect(config).to receive(:find_server_by).with({ :url => 'https://foo' }).and_return(nil)
          new_server = subject.select_a_server(nil, 'https://foo')
          expect(new_server).not_to be server
          expect(new_server.url).to eq 'https://foo'
        end
      end

      context 'url looks like a name' do
        it 'should return a server with that name if it has an url' do
          expect(config).to receive(:find_server).with('foo').and_return(server)
          expect(subject.select_a_server(nil, 'foo')).to be server
          expect(server.url).not_to be_nil
        end

        it 'should exit with error if a server with that name is found but it does not have an url' do
          server.url = nil
          expect(config).to receive(:find_server).with('foo').and_return(server)
          expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
          subject.select_a_server(nil, 'foo')
        end

        it 'should exit with error if a server with that name is not found' do
          expect(config).to receive(:find_server).with('foo').and_return(nil)
          expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
          subject.select_a_server(nil, 'foo')
        end
      end
    end
  end

  context 'with a server with a token in config' do
    before(:each) do
      allow(File).to receive(:read).and_return('
        {
          "current_server": null,
          "current_account": "kontena",
          "servers": [
            {
              "name": "fooserver",
              "url": "http://foo.example.com:80",
              "username": "admin",
              "grid": "test",
              "token": "abcd",
              "token_expires_at": 0,
              "refresh_token": null
            }
          ]
        }
      ')
      allow(File).to receive(:write).and_return(true)
      allow(File).to receive(:exist?).and_return(true)
      allow(File).to receive(:readable?).and_return(true)
      allow(Kontena::Client).to receive(:new).and_return(client)
      allow(Launchy).to receive(:open).and_return(true)
      allow(Kontena::LocalhostWebServer).to receive(:port).and_return(12345)
      allow(Kontena::LocalhostWebServer).to receive(:serve_one).and_return(
        { 'code' => 'abcd1234' }
      )
    end

    it 'logs in and changes the url of a named server in config' do
      expect(File).to receive(:write) do |fn, content|
        data = JSON.parse(content)
        expect(data['servers'].size).to eq 1
        expect(data['servers'].first['name']).to eq 'fooserver'
        expect(data['servers'].first['url']).to eq 'http://foo2.example.com'
        expect(data['servers'].first['token']).to eq 'abcdefg'
      end.and_return(true)
      expect(subject.config).to receive(:find_server).at_least(:once).with('fooserver').and_call_original
      expect(client).to receive(:authentication_ok?).and_return(true)
      subject.run(%w(--token abcdefg --name fooserver --grid foogrid http://foo2.example.com))
    end

    it 'logs in and creates a new entry in config' do
      expect(File).to receive(:write) do |fn, content|
        data = JSON.parse(content)
        expect(data['servers'].size).to eq 2
        expect(data['servers'].first['name']).to eq 'fooserver'
        expect(data['servers'].first['url']).to eq 'http://foo.example.com:80'
        expect(data['servers'].first['token']).to eq 'abcd'
        expect(data['servers'].last['name']).to eq 'fooserver2'
        expect(data['servers'].last['url']).to eq 'http://foo2.example.com'
        expect(data['servers'].last['token']).to eq 'abcdefg'
      end.and_return(true)
      expect(subject.config).to receive(:find_server).at_least(:once).with('fooserver2').and_call_original
      expect(client).to receive(:authentication_ok?).and_return(true)
      subject.run(%w(--token abcdefg --name fooserver2 --grid foogrid http://foo2.example.com))
    end

    it 'uses the existing token if it works and no --code --token --join or --force set when using url' do
      expect(client).to receive(:authentication_ok?).and_return(true)
      subject.run(%w(http://foo.example.com:80))
      expect(subject.config.servers.size).to eq 1
      expect(subject.config.current_server).to eq 'fooserver'
      expect(subject.config.servers.first.name).to eq 'fooserver'
      expect(subject.config.servers.first.url).to eq 'http://foo.example.com:80'
    end

    it 'uses the existing token if it works and no --code --token --join or --force set when using --name' do
      expect(client).to receive(:authentication_ok?).and_return(true)
      subject.run(%w(--name fooserver))
      expect(subject.config.servers.size).to eq 1
      expect(subject.config.current_server).to eq 'fooserver'
      expect(subject.config.servers.first.name).to eq 'fooserver'
      expect(subject.config.servers.first.url).to eq 'http://foo.example.com:80'
    end

    it 'uses the existing token if it works and no --code --token --join or --force set when using name in url param' do
      expect(client).to receive(:authentication_ok?).and_return(true)
      subject.run(%w(fooserver))
      expect(subject.config.servers.size).to eq 1
      expect(subject.config.current_server).to eq 'fooserver'
      expect(subject.config.servers.first.name).to eq 'fooserver'
      expect(subject.config.servers.first.url).to eq 'http://foo.example.com:80'
    end

    it 'uses current master and its token token if it works and no --code --token --join or --force set when no params' do
      subject.config.current_server = 'fooserver'
      expect(client).to receive(:authentication_ok?).and_return(true)
      subject.run([])
      expect(subject.config.servers.size).to eq 1
      expect(subject.config.current_server).to eq 'fooserver'
      expect(subject.config.servers.first.name).to eq 'fooserver'
      expect(subject.config.servers.first.url).to eq 'http://foo.example.com:80'
    end

    it 'goes to web flow when the existing token does not work' do
      expect(client).to receive(:authentication_ok?).and_return(false)
      expect(subject).to receive(:web_flow).and_return(true)
      subject.run(%w(--no-remote fooserver))
    end
  end

  context 'with no servers in config' do
    let(:webserver) { double(:webserver) }
    before(:each) do
      allow(File).to receive(:read).and_return('
        {
          "current_server": null,
          "current_account": "kontena",
          "servers": []
        }
      ')
      allow(File).to receive(:write).and_return(true)
      allow(File).to receive(:exist?).and_return(true)
      allow(File).to receive(:readable?).and_return(true)
      allow(Kontena::Client).to receive(:new).and_return(client)
      allow(Kontena::LocalhostWebServer).to receive(:new).and_return(webserver)
      allow(webserver).to receive(:port).and_return(12345)
      allow(webserver).to receive(:serve_one).and_return(
        { 'code' => 'abcd1234' }
      )
    end

    it 'creates a new entry when successful login using web flow' do
      expect(client).to receive(:last_response).at_least(:once).and_return(OpenStruct.new(status: 302, headers: { 'Location' => 'http://authprovider.example.com/authplz' }))
      expect(client).to receive(:request) do |opts|
        expect(opts[:path]).to eq "/authenticate?redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fcb&expires_in=7200"
        expect(opts[:http_method]).to eq :get
      end.and_return({})
      expect(Launchy).to receive(:open).with('http://authprovider.example.com/authplz').and_return(true)
      expect(client).to receive(:exchange_code).with('abcd1234').and_return('access_token' => 'defg456', 'server' => { 'name' => 'foobar' }, 'user' => { 'name' => 'testuser' })
      subject.run(%w(--no-remote --skip-grid-auto-select http://foobar.example.com))
      expect(subject.config.servers.size).to eq 1
      server = subject.config.servers.first
      expect(server.url).to eq 'http://foobar.example.com'
      expect(server.name).to eq 'foobar'
      expect(server.username).to eq 'testuser'
      expect(server.token.access_token).to eq 'defg456'
      expect(server.token.refresh_token).to be_nil
      expect(server.token.expires_at).to be_nil
    end

    it 'creates a new entry when successful login using --code' do
      expect(client).to receive(:exchange_code).with('defg').and_return('access_token' => 'defg456', 'server' => { 'name' => 'foobar' }, 'user' => { 'name' => 'testuser' })
      subject.run(%w(--skip-grid-auto-select --code defg http://foobar.example.com))
      expect(subject.config.servers.size).to eq 1
      server = subject.config.servers.first
      expect(server.url).to eq 'http://foobar.example.com'
      expect(server.name).to eq 'foobar'
      expect(server.username).to eq 'testuser'
      expect(server.token.access_token).to eq 'defg456'
      expect(server.token.refresh_token).to be_nil
      expect(server.token.expires_at).to be_nil
    end

    it 'asks for code when using --remote' do
      expect(client).to receive(:last_response).at_least(:once).and_return(OpenStruct.new(status: 302, headers: { 'Location' => 'http//authprovider.example.com/authplz' }))
      expect(client).to receive(:request) do |opts|
        expect(opts[:path]).to eq "/authenticate?redirect_uri=%2Fcode&expires_in=7200"
        expect(opts[:http_method]).to eq :get
      end.and_return({})
      expect(Kontena.prompt).to receive(:ask).and_return("abcd")
      expect(subject).to receive(:use_authorization_code).and_return('true')
      subject.run(%w(--remote http://foobar.example.com))
    end
  end

  context 'with servers in config' do
    let(:webserver) { double(:webserver) }
    before(:each) do
      allow(File).to receive(:read).and_return('
        {
          "current_server": null,
          "current_account": "kontena",
          "servers": [
            {
              "name": "fooserver",
              "url": "http://foo.example.com:80",
              "username": "admin",
              "grid": "test",
              "token": "abcd",
              "token_expires_at": 0,
              "refresh_token": null
            }
          ]
        }
      ')

      allow(File).to receive(:write).and_return(true)
      allow(File).to receive(:exist?).and_return(true)
      allow(File).to receive(:readable?).and_return(true)
      allow(Kontena::Client).to receive(:new).and_return(client)
      allow(Kontena::LocalhostWebServer).to receive(:new).and_return(webserver)
      allow(Kontena).to receive(:browserless?).and_return(false)
      allow(webserver).to receive(:port).and_return(12345)
      allow(webserver).to receive(:serve_one).and_return(
        { 'code' => 'abcd1234' }
      )
    end

    it 'changes current master to created master' do
      allow(client).to receive(:last_response).at_least(:once).and_return(OpenStruct.new(status: 302, headers: { 'Location' => 'http://authprovider.example.com/authplz' }))
      allow(client).to receive(:request).and_return({})
      allow(Launchy).to receive(:open).with('http://authprovider.example.com/authplz').and_return(true)
      allow(client).to receive(:exchange_code).with('abcd1234').and_return('access_token' => 'defg456', 'server' => { 'name' => 'foobar' }, 'user' => { 'name' => 'testuser' })
      subject.config.current_master = 'fooserver'
      subject.config.current_master
      subject.run(%w(--no-remote --skip-grid-auto-select http://foobar.example.com))
      expect(subject.config.current_master.name).to eq 'foobar'
    end
  end

  describe '#authentication_url_from_master' do
    it 'should exit with error if master returns a json with error' do
      allow(Kontena::Client).to receive(:new).and_return(client)
      expect(client).to receive(:last_response).at_least(:once).and_return(OpenStruct.new(status: 400, headers: {}))
      expect(client).to receive(:request).and_return('error' => 'no good')
      expect(subject).to receive(:exit_with_error).with(/no good/).and_throw(:exit_with_error)
      subject.authentication_url_from_master('https://foo.example.com', remote: true)
    end

    it 'should exit with error if master returns text' do
      allow(Kontena::Client).to receive(:new).and_return(client)
      expect(client).to receive(:last_response).at_least(:once).and_return(OpenStruct.new(status: 400, headers: {}))
      expect(client).to receive(:request).and_return('Fail!')
      expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
      subject.authentication_url_from_master('https://foo.example.com', remote: true)
    end

    it 'should exit with error if master returns nil' do
      allow(Kontena::Client).to receive(:new).and_return(client)
      expect(client).to receive(:last_response).at_least(:once).and_return(OpenStruct.new(status: 400, headers: {}))
      expect(client).to receive(:request).and_return(nil)
      expect(subject).to receive(:exit_with_error).with(/Invalid.+?400/).and_throw(:exit_with_error)
      subject.authentication_url_from_master('https://foo.example.com', remote: true)
    end
  end

  describe '#display_remote_message' do
    it 'should only print out the url if --silent' do
      subject.silent = true
      server = Kontena::Cli::Config::Server.new(url: 'https://foo', name: 'server')
      expect(subject).to receive(:authentication_url_from_master).and_return('http://foo')
      expect{subject.display_remote_message(server, {})}.to output("http://foo\n").to_stdout
    end
  end

  describe '#authentication_path' do
    it 'should raise if not doing --remote and the local port is missing' do
      expect{subject.authentication_path(local_port: nil, remote: false)}.to raise_error(ArgumentError)
    end
  end

  describe '#update_server_token' do
    let(:server) { Kontena::Cli::Config::Server.new(url: 'https://foo', name: 'server') }

    it 'should raise unless response is a hash' do
      expect{subject.update_server_token(server, nil)}.to raise_error(TypeError)
    end

    it 'should do a code exchange if response has "code"' do
      expect(subject).to receive(:use_authorization_code).with(server, "abcd")
      subject.update_server_token(server, "code" => "abcd")
    end

    it 'should exit with error if response has error' do
      expect(subject).to receive(:exit_with_error).and_throw(:exit_with_error)
      subject.update_server_token(server, "error" => "abcd")
    end

    it 'should update the token if all is good' do
      subject.update_server_token(server, 'access_token' => 'abcd1234', 'refresh_token' => 'defg', 'expires_at' => 1234)
      expect(server.token.access_token).to eq 'abcd1234'
      expect(server.token.refresh_token).to eq 'defg'
      expect(server.token.expires_at).to eq 1234
    end
  end

  describe '#update_server_name' do
    let(:server) { Kontena::Cli::Config::Server.new(url: 'https://foo', name: nil) }

    it 'should use a default name if response has no name' do
      subject.update_server_name(server, "foo" => "foofoo")
      expect(server.name).to eq 'kontena-master'
    end

    it 'should not mess with the server name if it already has one' do
      server.name = 'foofoo'
      subject.update_server_name(server, "server" => { "name" => "barbar" })
      expect(server.name).to eq 'foofoo'
    end
  end
end
