require 'abstract_unit'
require 'minitest/mock'
require 'rails/commands/dbconsole'

class Rails::DBConsoleTest < ActiveSupport::TestCase


  def setup
    Rails::DBConsole.const_set('APP_PATH', 'rails/all')
  end

  def teardown
    Rails::DBConsole.send(:remove_const, 'APP_PATH')
    %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)}
  end

  def test_config_with_db_config_only
    config_sample = {
      "test"=> {
        "adapter"=> "sqlite3",
        "host"=> "localhost",
        "port"=> "9000",
        "database"=> "foo_test",
        "user"=> "foo",
        "password"=> "bar",
        "pool"=> "5",
        "timeout"=> "3000"
      }
    }
    app_db_config(config_sample) do
      assert_equal config_sample["test"], Rails::DBConsole.new.config
    end
  end

  def test_config_with_no_db_config
    app_db_config(nil) do
      assert_raise(ActiveRecord::AdapterNotSpecified) {
        Rails::DBConsole.new.config
      }
    end
  end

  def test_config_with_database_url_only
    ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000'
    expected = {
      "adapter"  => "postgresql",
      "host"     => "localhost",
      "port"     => 9000,
      "database" => "foo_test",
      "username" => "foo",
      "password" => "bar",
      "pool"     => "5",
      "timeout"  => "3000"
    }.sort

    app_db_config(nil) do
      assert_equal expected, Rails::DBConsole.new.config.sort
    end
  end

  def test_config_choose_database_url_if_exists
    host = "database-url-host.com"
    ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000"
    sample_config = {
      "test" => {
        "adapter"  => "postgresql",
        "host"     => "not-the-#{host}",
        "port"     => 9000,
        "database" => "foo_test",
        "username" => "foo",
        "password" => "bar",
        "pool"     => "5",
        "timeout"  => "3000"
      }
    }
    app_db_config(sample_config) do
      assert_equal host, Rails::DBConsole.new.config["host"]
    end
  end

  def test_env
    assert_equal "test", Rails::DBConsole.new.environment

    ENV['RAILS_ENV'] = nil
    ENV['RACK_ENV'] = nil

    Rails.stub(:respond_to?, false) do
      assert_equal "development", Rails::DBConsole.new.environment

      ENV['RACK_ENV'] = "rack_env"
      assert_equal "rack_env", Rails::DBConsole.new.environment

      ENV['RAILS_ENV'] = "rails_env"
      assert_equal "rails_env", Rails::DBConsole.new.environment
    end
  ensure
    ENV['RAILS_ENV'] = "test"
    ENV['RACK_ENV'] = nil
  end

  def test_rails_env_is_development_when_argument_is_dev
    Rails::DBConsole.stub(:available_environments, ['development', 'test']) do
      options = Rails::DBConsole.send(:parse_arguments, ['dev'])
      assert_match('development', options[:environment])
    end
  end

  def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
    Rails::DBConsole.stub(:available_environments, ['dev']) do
      options = Rails::DBConsole.send(:parse_arguments, ['dev'])
      assert_match('dev', options[:environment])
    end
  end

  def test_mysql
    start(adapter: 'mysql2', database: 'db')
    assert !aborted
    assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args
  end

  def test_mysql_full
    start(adapter: 'mysql2', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8')
    assert !aborted
    assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args
  end

  def test_mysql_include_password
    start({adapter: 'mysql2', database: 'db', username: 'user', password: 'qwerty'}, ['-p'])
    assert !aborted
    assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args
  end

  def test_postgresql
    start(adapter: 'postgresql', database: 'db')
    assert !aborted
    assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
  end

  def test_postgresql_full
    start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432)
    assert !aborted
    assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
    assert_equal 'user', ENV['PGUSER']
    assert_equal 'host', ENV['PGHOST']
    assert_equal '5432', ENV['PGPORT']
    assert_not_equal 'q1w2e3', ENV['PGPASSWORD']
  end

  def test_postgresql_include_password
    start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p'])
    assert !aborted
    assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
    assert_equal 'user', ENV['PGUSER']
    assert_equal 'q1w2e3', ENV['PGPASSWORD']
  end

  def test_sqlite3
    start(adapter: 'sqlite3', database: 'db.sqlite3')
    assert !aborted
    assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
  end

  def test_sqlite3_mode
    start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html'])
    assert !aborted
    assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
  end

  def test_sqlite3_header
    start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header'])
    assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
  end

  def test_sqlite3_db_absolute_path
    start(adapter: 'sqlite3', database: '/tmp/db.sqlite3')
    assert !aborted
    assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args
  end

  def test_sqlite3_db_without_defined_rails_root
    Rails.stub(:respond_to?, false) do
      start(adapter: 'sqlite3', database: 'config/db.sqlite3')
      assert !aborted
      assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
    end
  end

  def test_oracle
    start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret')
    assert !aborted
    assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args
  end

  def test_oracle_include_password
    start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p'])
    assert !aborted
    assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args
  end

  def test_unknown_command_line_client
    start(adapter: 'unknown', database: 'db')
    assert aborted
    assert_match(/Unknown command-line client for db/, output)
  end

  def test_print_help_short
    stdout = capture(:stdout) do
      start({}, ['-h'])
    end
    assert aborted
    assert_equal '', output
    assert_match(/Usage:.*dbconsole/, stdout)
  end

  def test_print_help_long
    stdout = capture(:stdout) do
      start({}, ['--help'])
    end
    assert aborted
    assert_equal '', output
    assert_match(/Usage:.*dbconsole/, stdout)
  end

  attr_reader :aborted, :output
  private :aborted, :output

  private

  def app_db_config(results)
    Rails.application.config.stub(:database_configuration, results || {}) do
      yield
    end
  end

  def dbconsole
    @dbconsole ||= Class.new(Rails::DBConsole) do
      attr_reader :find_cmd_and_exec_args

      def find_cmd_and_exec(*args)
        @find_cmd_and_exec_args = args
      end
    end.new(nil)
  end

  def start(config = {}, argv = [])
    dbconsole.stub(:config, config.stringify_keys) do
      dbconsole.stub(:arguments, argv) do
        capture_abort { dbconsole.start }
      end
    end
  end

  def capture_abort
    @aborted = false
    @output = capture(:stderr) do
      begin
        yield
      rescue SystemExit
        @aborted = true
      end
    end
  end

end
