# frozen_string_literal: true

require 'spec_helper'
require 'dump'
require 'tmpdir'

class Chicken < ActiveRecord::Base
end

ActiveRecord::Base.logger = Logger.new(File.join(Dump.rails_root, 'log/dump.log'))

def database_configs
  YAML.load_file(File.expand_path('../db/database.yml', __FILE__))
end

def adapters
  database_configs.keys
end

def use_adapter(adapter)
  config = database_configs[adapter]
  begin
    case config['adapter']
    when /^mysql/
      ActiveRecord::Base.establish_connection(config.merge('database' => nil))
      ActiveRecord::Base.connection.drop_database config['database']
      ActiveRecord::Base.connection.create_database(config['database'], :charset => 'utf8')
    when /^postgresql/
      ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
      ActiveRecord::Base.connection.drop_database config['database']
      ActiveRecord::Base.connection.create_database(config['database'])
    end
    ActiveRecord::Base.establish_connection(config)
    load_schema
    yield
  ensure
    case config['adapter']
    when /^mysql/
      ActiveRecord::Base.establish_connection(config)
      ActiveRecord::Base.connection.drop_database config['database']
    when /^postgresql/
      ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
      ActiveRecord::Base.connection.drop_database config['database']
    end
  end
ensure
  ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
end

def schema_path
  File.expand_path('../db/schema.rb', __FILE__)
end

def load_schema
  grab_output do
    load(schema_path)
  end
end

def create_chickens!(options = {})
  time = Time.local(2000, 'jan', 1, 20, 15, 1)
  times = [time - 1_000_000, time, time + 1_000_000]
  data = {
    :string => ['', 'lala'],
    :text => ['', 'lala', 'lala' * 100],
    :integer => [-1000, 0, 1000],
    :float => [-1000.0, 0.0, 1000.0],
    :decimal => [-1000.0, 0.0, 1000.0],
    :datetime => times.map(&:to_datetime),
    :timestamp => times,
    :time => times,
    :date => times.map(&:to_date),
    :boolean => [true, false],
  }
  Chicken.create!
  data.values.map(&:length).max.times do |i|
    Chicken.create! do |chicken|
      data.each do |type, values|
        chicken["#{type}_col"] = values[i]
      end
    end
  end
  options[:random].to_i.times do
    Chicken.create! do |chicken|
      data.each do |type, values|
        chicken["#{type}_col"] = values[rand(values.length)] if rand > 0.5
      end
    end
  end
end

def chicken_data
  Chicken.all.map(&:attributes).sort_by{ |attributes| attributes['id'] }
end

def reset_rake!
  @rake = Rake::Application.new
  Rake.application = @rake
  load 'tasks/assets.rake'
  load 'tasks/dump.rake'
  Rake::Task.define_task('environment')
  Rake::Task.define_task('db:schema:dump') do
    File.open(schema_path, 'r') do |r|
      if ENV['SCHEMA']
        File.write(ENV.fetch('SCHEMA', nil), r.read)
      end
    end
  end
  Rake::Task.define_task('db:schema:load') do
    load(ENV.fetch('SCHEMA', nil))
  end
end

def call_rake(&block)
  reset_rake!
  grab_output(&block)
end

def call_rake_create(*args)
  call_rake do
    Dump.create(*args)
  end
end

def call_rake_restore(*args)
  call_rake do
    Dump.restore(*args)
  end
end

describe 'full cycle' do
  around do |example|
    Dir.mktmpdir do |dir|
      @tmp_dir = dir
      example.run
    end
  end
  before do
    allow(Dump).to receive(:rails_root).and_return(@tmp_dir)
    allow(Progress).to receive(:io).and_return(StringIO.new)
  end

  begin
    database_configs

    adapters.each do |adapter|
      it "dumps and restores using #{adapter}" do
        use_adapter(adapter) do
          # add chickens store their attributes and create dump
          create_chickens!(:random => 100)
          saved_chicken_data = chicken_data
          call_rake_create(:description => 'chickens')

          # clear database
          load_schema
          expect(Chicken.all).to eq([])

          # restore dump and verify equality
          call_rake_restore(:version => 'chickens')
          expect(chicken_data).to eq(saved_chicken_data)

          # go throught create/restore cycle and verify equality
          call_rake_create
          load_schema
          expect(Chicken.all).to be_empty
          call_rake_restore
          expect(chicken_data).to eq(saved_chicken_data)
        end
      end

      it "does not break id incrementing using #{adapter}" do
        use_adapter(adapter) do
          create_chickens!(:random => 100)
          call_rake_create(:description => 'chickens')
          load_schema
          call_rake_restore(:version => 'chickens')
          create_chickens!
        end
      end
    end

    adapters.permutation(2) do |adapter_src, adapter_dst|
      it "dumps using #{adapter_src} and restores using #{adapter_dst}" do
        saved_chicken_data = nil
        use_adapter(adapter_src) do
          expect(Chicken.all).to be_empty

          create_chickens!(:random => 100)
          saved_chicken_data = chicken_data
          call_rake_create
        end

        use_adapter(adapter_dst) do
          expect(Chicken.all).to be_empty

          call_rake_restore
          expect(chicken_data).to eq(saved_chicken_data)
        end
      end
    end

    it 'creates same dump for all adapters' do
      dumps = []
      adapters.each do |adapter|
        use_adapter(adapter) do
          dump_name = call_rake_create(:desc => adapter)[:stdout].strip
          dump_path = File.join(Dump.rails_root, 'dump', dump_name)

          data = []
          Archive::Tar::Minitar.open(Zlib::GzipReader.open(dump_path), 'r') do |stream|
            stream.each do |entry|
              entry_data = if entry.full_name == 'schema.rb'
                entry.read
              else
                Marshal.load(entry.read)
              end
              data << [entry.full_name, entry_data]
            end
          end
          dumps << {:path => dump_path, :data => data.sort}
        end
      end

      dumps.combination(2) do |dump_a, dump_b|
        expect(dump_a[:path]).not_to eq(dump_b[:path])
        expect(dump_a[:data]).to eq(dump_b[:data])
      end
    end
  rescue Errno::ENOENT => e
    $stderr.puts e
  end
end
