# frozen_string_literal: true
require 'rbconfig'
require 'yaml'

if ENV['COVERAGE']
  require_relative "sequel_coverage"
  SimpleCov.sequel_coverage(:subprocesses=>true)
end

RUBY = RbConfig.ruby
OUTPUT = "spec/bin-sequel-spec-output-#{$$}.log"
TMP_FILE = "spec/bin-sequel-tmp-#{$$}.rb"
BIN_SPEC_DB = "spec/bin-sequel-spec-db-#{$$}.sqlite3"
BIN_SPEC_DB2 = "spec/bin-sequel-spec-db2-#{$$}.sqlite3"

if RUBY_ENGINE == 'jruby'
  CONN_PREFIX = 'jdbc:sqlite:'
  CONN_HASH = {:adapter=>'jdbc', :uri=>"#{CONN_PREFIX}#{BIN_SPEC_DB}"}
else
  CONN_PREFIX = 'sqlite://'
  CONN_HASH = {:adapter=>'sqlite', :database=>BIN_SPEC_DB}
end

require_relative '../lib/sequel'

Sequel::DB = nil
File.delete(BIN_SPEC_DB) if File.file?(BIN_SPEC_DB)
File.delete(BIN_SPEC_DB2) if File.file?(BIN_SPEC_DB2)
DB = Sequel.connect("#{CONN_PREFIX}#{BIN_SPEC_DB}", :test=>false)
DB2 = Sequel.connect("#{CONN_PREFIX}#{BIN_SPEC_DB2}", :test=>false)

ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
gem 'minitest'
require 'minitest/global_expectations/autorun'

describe "bin/sequel" do
  def bin(opts={})
    cmd = "#{opts[:pre]}\"#{RUBY}\" -I lib spec/bin_shim #{opts[:args]} #{"#{CONN_PREFIX}#{BIN_SPEC_DB}" unless opts[:no_conn]} #{opts[:post]}> #{OUTPUT}#{" 2>&1" if opts[:stderr]}"
    system(cmd)
    File.read(OUTPUT)
  end

  int_type = DB.sqlite_version >= 33700 ? "INTEGER" : 'integer'

  after do
    DB.disconnect
    DB2.disconnect
    [BIN_SPEC_DB, BIN_SPEC_DB2, TMP_FILE, OUTPUT].each do |file|
      if File.file?(file)
        begin
          File.delete(file)
        rescue Errno::ENOENT, Errno::EACCES
          nil
        end
      end
    end
  end
  
  it "-h should print the help" do
    help = bin(:args=>"-h", :no_conn=>true)
    help.must_match(/\ASequel: The Database Toolkit for Ruby/)
    help.must_match(/^Usage: sequel /)
  end

  it "-c should run code" do
    bin(:args=>'-c "print DB.tables.inspect"').must_equal '[]'
    DB.create_table(:a){Integer :a}
    bin(:args=>'-c "print DB.tables.inspect"').must_equal '[:a]'
    bin(:args=>'-v -c "print DB.tables.inspect"').strip.must_equal "sequel #{Sequel.version}\n[:a]"
  end

  it "-C should copy databases" do
    DB.create_table(:a) do
      primary_key :a
      String :name
    end
    DB.create_table(:b) do
      foreign_key :a, :a
      index :a
    end
    DB[:a].insert(1, 'foo')
    DB[:b].insert(1)
    bin(:args=>'-C', :post=>"#{CONN_PREFIX}#{BIN_SPEC_DB2}").must_match Regexp.new(<<END)
Databases connections successful
Migrations dumped successfully
Tables created
Begin copying data
Begin copying records for table: a
Finished copying 1 records for table: a
Begin copying records for table: b
Finished copying 1 records for table: b
Finished copying data
Begin creating indexes
Finished creating indexes
Begin adding foreign key constraints
Finished adding foreign key constraints
Database copy finished in \\d+\\.\\d+ seconds
END
    DB2.tables.sort_by{|t| t.to_s}.must_equal [:a, :b]
    DB[:a].all.must_equal [{:a=>1, :name=>'foo'}]
    DB[:b].all.must_equal [{:a=>1}]
    DB2.schema(:a).map{|col, sch| [col, *sch.values_at(:allow_null, :default, :primary_key, :db_type, :type, :ruby_default)]}.must_equal [[:a, false, nil, true, int_type, :integer, nil], [:name, true, nil, false, "varchar(255)", :string, nil]]
    DB2.schema(:b).map{|col, sch| [col, *sch.values_at(:allow_null, :default, :primary_key, :db_type, :type, :ruby_default)]}.must_equal [[:a, true, nil, false, int_type, :integer, nil]]
    DB2.indexes(:a).must_equal({})
    DB2.indexes(:b).must_equal(:b_a_index=>{:unique=>false, :columns=>[:a]})
    DB2.foreign_key_list(:a).must_equal []
    DB2.foreign_key_list(:b).must_equal [{:columns=>[:a], :table=>:a, :key=>nil, :on_update=>:no_action, :on_delete=>:no_action}]
  end

  it "-C should copy databases showing status while iterating over tables" do
    DB.create_table(:a) do
      primary_key :a
      String :name
    end
    DB.create_table(:b) do
      foreign_key :a, :a
      index :a
    end
    DB[:a].insert(1, 'foo')

    begin
      ENV['SEQUEL_BIN_STATUS_ALL_LINES'] = '1'
      bin(:args=>'-C', :post=>"#{CONN_PREFIX}#{BIN_SPEC_DB2}").must_match Regexp.new(<<END)
Databases connections successful
Migrations dumped successfully
Tables created
Begin copying data
Begin copying records for table: a
Status: 1 records copied
Finished copying 1 records for table: a
Begin copying records for table: b
Finished copying 0 records for table: b
Finished copying data
Begin creating indexes
Finished creating indexes
Begin adding foreign key constraints
Finished adding foreign key constraints
Database copy finished in \\d+\\.\\d+ seconds
END
    ensure
      ENV.delete('SEQUEL_BIN_STATUS_ALL_LINES')
    end

    DB2.tables.sort_by{|t| t.to_s}.must_equal [:a, :b]
    DB[:a].all.must_equal [{:a=>1, :name=>'foo'}]
    DB[:b].all.must_equal []
    DB2.schema(:a).map{|col, sch| [col, *sch.values_at(:allow_null, :default, :primary_key, :db_type, :type, :ruby_default)]}.must_equal [[:a, false, nil, true, int_type, :integer, nil], [:name, true, nil, false, "varchar(255)", :string, nil]]
    DB2.schema(:b).map{|col, sch| [col, *sch.values_at(:allow_null, :default, :primary_key, :db_type, :type, :ruby_default)]}.must_equal [[:a, true, nil, false, int_type, :integer, nil]]
    DB2.indexes(:a).must_equal({})
    DB2.indexes(:b).must_equal(:b_a_index=>{:unique=>false, :columns=>[:a]})
    DB2.foreign_key_list(:a).must_equal []
    DB2.foreign_key_list(:b).must_equal [{:columns=>[:a], :table=>:a, :key=>nil, :on_update=>:no_action, :on_delete=>:no_action}]
  end

  it "-C should display error if not given second database" do
    bin(:args=>'-C', :stderr=>true).must_include 'Error: Must specify database connection string or path to yaml file as second argument for database you want to copy to'
  end

  it "-C should convert integer to bigint when copying from SQLite to other databases" do
    DB.create_table(:a) do
      Integer :id
    end
    bin(:args=>'-EC', :post=>"mock://postgres").must_include 'CREATE TABLE "a" ("id" bigint)'
  end

  it "-d and -D should dump generic and specific migrations" do
    DB.create_table(:a) do
      primary_key :a
      String :name
    end
    DB.create_table(:b) do
      foreign_key :a, :a
      index :a
    end
    size = RUBY_VERSION >= '3.4' ? 'size: 255' : ':size=>255'
    ignore_index_errors = RUBY_VERSION >= '3.4' ? 'ignore_index_errors: true' : ':ignore_index_errors=>true'
    bin(:args=>'-d').must_equal <<END
Sequel.migration do
  change do
    create_table(:a) do
      primary_key :a
      String :name, #{size}
    end
    
    create_table(:b, #{ignore_index_errors}) do
      foreign_key :a, :a
      
      index [:a]
    end
  end
end
END
    bin(:args=>'-D').must_equal <<END
Sequel.migration do
  change do
    create_table(:a) do
      primary_key :a
      column :name, "varchar(255)"
    end
    
    create_table(:b) do
      foreign_key :a, :a
      
      index [:a]
    end
  end
end
END
  end

  it "-E should echo SQL statements to stdout" do
    bin(:args=>'-E -c DB.tables').must_include "SELECT * FROM `sqlite_master` WHERE ((`name` != 'sqlite_sequence') AND (`type` = 'table'))"
  end

  it "-I should include directory in load path" do
    bin(:args=>'-Ifoo -c "p 1 if $:.include?(\'foo\')"').must_equal "1\n"
  end

  it "-l should log SQL statements to file" do
    bin(:args=>"-l #{TMP_FILE} -c DB.tables").must_equal ''
    File.read(TMP_FILE).must_include "SELECT * FROM `sqlite_master` WHERE ((`name` != 'sqlite_sequence') AND (`type` = 'table'))"
  end

  it "-L should load all *.rb files in given directory" do
    bin(:args=>'-r ./lib/sequel/extensions/migration -L ./spec/files/integer_migrations -c "p Sequel::Migration.descendants.length"').must_equal "3\n"
  end

  it "-m should migrate database up" do
    bin(:args=>"-m spec/files/integer_migrations").must_equal ''
    DB.tables.sort_by{|t| t.to_s}.must_equal [:schema_info, :sm1111, :sm2222, :sm3333]
  end

  it "-M should specify version to migrate to" do
    bin(:args=>"-m spec/files/integer_migrations -M 2").must_equal ''
    DB.tables.sort_by{|t| t.to_s}.must_equal [:schema_info, :sm1111, :sm2222]
  end

  it "-N should not test for a valid connection" do
    bin(:no_conn=>true, :args=>"-c '' -N #{CONN_PREFIX}spec/nonexistent/foo").must_equal ''
    bin(:no_conn=>true, :args=>"-c '' #{CONN_PREFIX}spec/nonexistent/foo", :stderr=>true).must_match(/\AError: Sequel::DatabaseConnectionError: /)
  end

  it "-r should require a given library" do
    bin(:args=>'-rsequel/extensions/sql_expr -c "print DB.literal(1.sql_expr)"').must_equal "1"
  end

  it "-S should dump the schema cache" do
    bin(:args=>"-S #{TMP_FILE}").must_equal ''
    Marshal.load(File.read(TMP_FILE)).must_equal({})
    DB.create_table(:a){Integer :a}
    bin(:args=>"-S #{TMP_FILE}").must_equal ''
    h = Marshal.load(File.read(TMP_FILE))
    h.keys.must_equal ['`a`']
    column, schema = h.values.first.first
    column.must_equal :a
    schema[:type].must_equal :integer
    schema[:db_type].must_equal int_type
    schema.fetch(:ruby_default).must_be_nil
    schema.fetch(:default).must_be_nil
    schema[:allow_null].must_equal true
    schema[:primary_key].must_equal false
  end

  it "-X should dump the index cache" do
    bin(:args=>"-X #{TMP_FILE}").must_equal ''
    Marshal.load(File.read(TMP_FILE)).must_equal({})
    DB.create_table(:a){Integer :id}
    DB.create_table(:b){Integer :b, index: {name: "idx_test", unique: true}}
    bin(:args=>"-X #{TMP_FILE}").must_equal ''
    Marshal.load(File.read(TMP_FILE)).must_equal("`a`"=>{}, "`b`"=>{:idx_test=>{:unique=>true, :columns=>[:b]}})
  end

  it "-t should output full backtraces on error" do
    bin(:args=>'-c "lambda{lambda{lambda{raise \'foo\'}.call}.call}.call"', :stderr=>true).count("\n").must_be :<,  3
    bin(:args=>'-t -c "lambda{lambda{lambda{raise \'foo\'}.call}.call}.call"', :stderr=>true).count("\n").must_be :>,  3
  end

  it "-v should output the Sequel version and exit if database is not given" do
    bin(:args=>"-v", :no_conn=>true).strip.must_equal "sequel #{Sequel.version}"
  end

  it "should error if using -M without -m" do
    bin(:args=>'-M 2', :stderr=>true).must_equal "Error: Must specify -m if using -M\n"
  end

  it "should error if using mutually exclusive options together" do
    bin(:args=>'-c foo -d', :stderr=>true).must_equal "Error: Cannot specify -c and -d together\n"
    bin(:args=>'-D -d', :stderr=>true).must_equal "Error: Cannot specify -D and -d together\n"
    bin(:args=>'-m foo -d', :stderr=>true).must_equal "Error: Cannot specify -m and -d together\n"
    bin(:args=>'-S foo -d', :stderr=>true).must_equal "Error: Cannot specify -S and -d together\n"
    bin(:args=>'-S foo -C', :stderr=>true).must_equal "Error: Cannot specify -S and -C together\n"
  end

  it "should warn if providing too many arguments" do
    bin(:args=>'-c "" "" 1 2 3 4', :stderr=>true).must_equal "Warning: last 5 arguments ignored\n"
  end

  it "should use a mock database if no database is given" do
    bin(:args=>'-c "print DB.adapter_scheme"', :no_conn=>true).must_equal "mock"
  end

  it "should work with a yaml config file" do
    File.open(TMP_FILE, 'wb'){|f| f.write(YAML.dump(CONN_HASH))}
    bin(:args=>"-c \"print DB.tables.inspect\" #{TMP_FILE}", :no_conn=>true).must_equal "[]"
    DB.create_table(:a){Integer :a}
    bin(:args=>"-c \"print DB.tables.inspect\" #{TMP_FILE}", :no_conn=>true).must_equal "[:a]"
  end

  it "should work with a yaml config file with string keys" do
    h = {}
    CONN_HASH.each{|k,v| h[k.to_s] = v}
    File.open(TMP_FILE, 'wb'){|f| f.write(YAML.dump(h))}
    DB.create_table(:a){Integer :a}
    bin(:args=>"-c \"print DB.tables.inspect\" #{TMP_FILE}", :no_conn=>true).must_equal "[:a]"
  end

  it "should work with a yaml config file with environments" do
    File.open(TMP_FILE, 'wb'){|f| f.write(YAML.dump(:development=>CONN_HASH))}
    bin(:args=>"-c \"print DB.tables.inspect\" #{TMP_FILE}", :no_conn=>true).must_equal "[]"
    DB.create_table(:a){Integer :a}
    bin(:args=>"-c \"print DB.tables.inspect\" #{TMP_FILE}", :no_conn=>true).must_equal "[:a]"
  end

  it "-e should set environment for yaml config file" do
    File.open(TMP_FILE, 'wb'){|f| f.write(YAML.dump(:foo=>CONN_HASH))}
    bin(:args=>"-c \"print DB.tables.inspect\" -e foo #{TMP_FILE}", :no_conn=>true).must_equal "[]"
    DB.create_table(:a){Integer :a}
    bin(:args=>"-c \"print DB.tables.inspect\" -e foo #{TMP_FILE}", :no_conn=>true).must_equal "[:a]"
    File.open(TMP_FILE, 'wb'){|f| f.write(YAML.dump('foo'=>CONN_HASH))}
    bin(:args=>"-c \"print DB.tables.inspect\" -e foo #{TMP_FILE}", :no_conn=>true).must_equal "[:a]"
  end

  it "should run code in given filenames" do
    File.open(TMP_FILE, 'wb'){|f| f.write('print DB.tables.inspect')}
    bin(:post=>TMP_FILE).must_equal '[]'
    DB.create_table(:a){Integer :a}
    bin(:post=>TMP_FILE).must_equal '[:a]'
    bin(:post=>TMP_FILE, :args=>'-v').strip.must_equal "sequel #{Sequel.version}\n[:a]"
  end

  it "should run code provided on stdin" do
    bin(:pre=>'echo print DB.tables.inspect | ').must_equal '[]'
    DB.create_table(:a){Integer :a}
    bin(:pre=>'echo print DB.tables.inspect | ').must_equal '[:a]'
  end
end
