require_relative '../../../spec/spec_helper.rb'
require_relative '../import_user'
require_relative '../export_user'
require_relative '../../../lib/carto/ghost_tables_manager'

RSpec.configure do |c|
  c.include Helpers
end
describe CartoDB::DataMover::ExportJob do
  def stub_api_keys
    CartoDB::DataMover::ImportJob.any_instance.stubs(:create_org_api_key_roles)
    CartoDB::DataMover::ImportJob.any_instance.stubs(:create_user_api_key_roles)
    CartoDB::DataMover::ImportJob.any_instance.stubs(:grant_org_api_key_roles)
    CartoDB::DataMover::ImportJob.any_instance.stubs(:grant_user_api_key_roles)
    CartoDB::DataMover::ImportJob.any_instance.stubs(:create_user_oauth_app_user_roles)
    CartoDB::DataMover::ImportJob.any_instance.stubs(:grant_user_oauth_app_user_roles)
  end

  before :each do
    bypass_named_maps
    @tmp_path = Dir.mktmpdir("mover-test") + '/'
  end

  let(:first_user) do
    create_user(
      quota_in_bytes: 100.megabyte,
      private_tables_enabled: true,
      database_timeout: 123450,
      user_timeout: 456780,
      table_quota: nil
    )
  end

  shared_examples "a migrated user" do
    it "has expected statement_timeout" do
      expect(subject.in_database['SHOW statement_timeout'].to_a[0][:statement_timeout]).to eq("456780ms")
      expect(subject.in_database(as: :public_db_user)['SHOW statement_timeout']
        .to_a[0][:statement_timeout]).to eq("123450ms")
    end

    it "keeps proper table privacy" do
      check_tables(subject)
    end
  end

  describe "a migrated user" do
    before(:each) do
      stub_api_keys
    end

    subject do
      create_tables(first_user)
      first_user.save
      first_user.reload
      carto_user = Carto::User.find(first_user.id)
      Carto::UserNotification.create!(user: carto_user, notifications: { builder: { onboarding: true } })
      move_user(first_user)
    end

    it_behaves_like "a migrated user"
    it "matches old and new user" do
      expect((first_user.as_json.reject { |x| [:updated_at, :database_host].include?(x.to_sym) }))
        .to eq((subject.as_json.reject { |x| [:updated_at, :database_host].include?(x.to_sym) }))
    end
  end

  describe "a standalone user which has moved to an organization" do
    before(:all) do
      @org = create_user_mover_test_organization
    end

    subject do
      create_tables(first_user)
      first_user.db_service.move_to_own_schema

      CartoDB::DataMover::ExportJob.new(id: first_user.username, path: @tmp_path, schema_mode: true)
      CartoDB::UserModule::DBService.terminate_database_connections(first_user.database_name, first_user.database_host)
      CartoDB::DataMover::ImportJob.new(
        file: @tmp_path + "user_#{first_user.id}.json", mode: :rollback, drop_database: true, drop_roles: true).run!
      CartoDB::DataMover::ImportJob.new(
        file: @tmp_path + "user_#{first_user.id}.json", mode: :import, host: '127.0.0.2', target_org: @org.name).run!

      moved_user = ::User[first_user.id]
      moved_user.reload # Clean user sequel cache

      Carto::GhostTablesManager.new(moved_user.id).link_ghost_tables_synchronously
      moved_user
    end

    it_behaves_like "a migrated user"

    it "matches old and new user except database_name and database_host" do
      IGNORED_KEYS = [:updated_at, :database_name, :database_host, :organization_id, :database_schema].freeze
      expect(first_user.as_json.reject { |x| IGNORED_KEYS.include? x.to_sym })
        .to eq(subject.as_json.reject { |x| IGNORED_KEYS.include? x.to_sym })
      expect(subject.database_name).to eq(@org.owner.database_name)
      expect(subject.database_schema).to eq(subject.username)
      expect(subject.database_host).to eq('127.0.0.2')
      expect(subject.organization_id).to eq(@org.id)
    end

    it "has granted the org role" do
      authed_roles = subject
                      .in_database["select s.rolname from pg_roles r
                        join pg_catalog.pg_auth_members m on r.oid=m.member join pg_catalog.pg_roles
                        s on m.roleid=s.oid where r.rolname='#{subject.database_username}'"].to_a
      authed_roles.should be_any { |m| m[:rolname] == subject.db_service.organization_member_group_role_member_name }
    end
  end

  it "should move a user from an organization to its own account" do
    org = create_user_mover_test_organization
    user = create_user(
      quota_in_bytes: 100.megabyte, table_quota: 400, organization: org, account_type: 'ORGANIZATION USER'
    )
    org.reload
    create_tables(user)

    CartoDB::DataMover::ExportJob.new(id: user.username, path: @tmp_path, schema_mode: true)
    CartoDB::UserModule::DBService.terminate_database_connections(user.database_name, user.database_host)
    CartoDB::DataMover::ImportJob.new(file: @tmp_path + "user_#{user.id}.json", mode: :rollback).run!
    CartoDB::DataMover::ImportJob.new(file: @tmp_path + "user_#{user.id}.json", mode: :import, host: '127.0.0.2').run!

    moved_user = ::User.find(username: user.username)
    moved_user.reload
    Carto::GhostTablesManager.new(moved_user.id).link_ghost_tables_synchronously
    check_tables(moved_user)
    moved_user.organization_id.should eq nil
  end

  # Skipping this test (TODO: fix it)
  xit "should move a whole organization" do
    port = find_available_port
    run_test_server(port)

    test_config = Cartodb.get_config(:org_metadata_api).merge('host' => '127.0.0.1', 'port' => port)
    Cartodb.with_config(org_metadata_api: test_config) do
      org = create_user_mover_test_organization
      user = create_user(
        quota_in_bytes: 100.megabyte, table_quota: 400, organization: org, account_type: 'ORGANIZATION USER'
      )
      user.save
      org.reload

      create_tables(org.owner)
      share_tables(org.owner, user)
      share_tables(user, org.owner)
      create_tables(user)

      carto_org = Carto::Organization.find(org.id)
      group_1 = carto_org.create_group(String.random(5))
      group_2 = carto_org.create_group(String.random(5))

      group_1.add_user(org.owner.username)
      group_2.add_user(org.owner.username)
      group_1.add_user(user.username)
      group_2.add_user(user.username)

      share_group_tables(org.owner, group_1)
      share_group_tables(user, group_2)

      CartoDB::DataMover::ExportJob.new(organization_name: org.name, path: @tmp_path, split_user_schemas: false)
      CartoDB::UserModule::DBService.terminate_database_connections(org.owner.database_name, org.owner.database_host)
      CartoDB::DataMover::ImportJob.new(file: @tmp_path + "org_#{org.id}.json", mode: :rollback, drop_database: true, drop_roles: true).run!
      CartoDB::DataMover::ImportJob.new(file: @tmp_path + "org_#{org.id}.json", mode: :import, host: '127.0.0.2').run!

      moved_user = ::User.find(username: user.username)
      moved_user.reload
      moved_user.database_host.should eq '127.0.0.2'
      Carto::GhostTablesManager.new(moved_user.id).link_ghost_tables_synchronously
      check_tables(moved_user)
      moved_user.in_database['SELECT * FROM cdb_tablemetadata']
      moved_user.organization_id.should_not eq nil
    end

    @server_thread.terminate
  end

  it "should move a whole organization without splitting user schemas" do

    org = create_user_mover_test_organization
    user = create_user(
      quota_in_bytes: 100.megabyte, table_quota: 400, organization: org, account_type: 'ORGANIZATION USER'
    )
    user.save
    org.reload

    create_tables(org.owner)
    share_tables(org.owner, user)
    share_tables(user, org.owner)
    create_tables(user)

    CartoDB::DataMover::ExportJob.new(organization_name: org.name, path: @tmp_path)
    CartoDB::UserModule::DBService.terminate_database_connections(org.owner.database_name, org.owner.database_host)
    CartoDB::DataMover::ImportJob.new(file: @tmp_path + "org_#{org.id}.json", mode: :rollback, drop_database: true, drop_roles: true).run!
    CartoDB::DataMover::ImportJob.new(file: @tmp_path + "org_#{org.id}.json", mode: :import, host: '127.0.0.2').run!

    moved_user = ::User.find(username: user.username)
    moved_user.reload
    moved_user.database_host.should eq '127.0.0.2'
    Carto::GhostTablesManager.new(moved_user.id).link_ghost_tables_synchronously
    check_tables(moved_user)
    moved_user.in_database['SELECT * FROM cdb_tablemetadata']
    moved_user.organization_id.should_not eq nil

  end

  it "should not touch an user metadata nor update its oids when update_metadata is not set" do
    stub_api_keys
    user = create_user(
      quota_in_bytes: 100.megabyte, table_quota: 50, private_tables_enabled: true, sync_tables_enabled: true
    )
    create_tables(user)

    old_user_data = user.as_json
    old_user_tables = user.real_tables
    old_user_oids = user.tables.map(&:table_id).sort
    CartoDB::DataMover::ExportJob.new(id: user.username, path: @tmp_path)
    CartoDB::UserModule::DBService.terminate_database_connections(user.database_name, user.database_host)
    CartoDB::DataMover::ImportJob.new(
      file: @tmp_path + "user_#{user.id}.json", mode: :rollback, host: '127.0.0.2',
      drop_database: true, drop_roles: true).run!
    CartoDB::DataMover::ImportJob.new(file: @tmp_path + "user_#{user.id}.json", mode: :import, host: '127.0.0.2',
                                      update_metadata: false).run!

    new_user = ::User.find(username: user.username)

    new_user.as_json.should eq old_user_data
    new_user.real_tables.should_not eq old_user_tables
    new_user.tables.map(&:table_id).sort.should eq old_user_oids
  end
end

module Helpers
  def move_user(user)
    CartoDB::DataMover::ExportJob.new(id: user.username, path: @tmp_path)
    CartoDB::UserModule::DBService.terminate_database_connections(user.database_name, user.database_host)
    CartoDB::DataMover::ImportJob.new(
      file: @tmp_path + "user_#{user.id}.json", mode: :rollback,
      drop_database: true, drop_roles: true).run!
    CartoDB::DataMover::ImportJob.new(file: @tmp_path + "user_#{user.id}.json", mode: :import, host: '127.0.0.2').run!
    ::User.find(username: user.username)
  end

  def create_tables(user)
    create_table(user_id: user.id, name: "with_link_table", privacy: UserTable::PRIVACY_LINK)
    create_table(user_id: user.id, name: "public_table",    privacy: UserTable::PRIVACY_PUBLIC)
    create_table(user_id: user.id, name: "private_table",   privacy: UserTable::PRIVACY_PRIVATE)
  end

  def share_tables(user1, user2)
    table_ro = create_table(user_id: user1.id, name: "shared_ro_by_#{user1.username}_to_#{user2.id}",
      privacy: UserTable::PRIVACY_PRIVATE)
    give_permission(table_ro.table_visualization, user2, Carto::Permission::ACCESS_READONLY)
    table_rw = create_table(user_id: user1.id, name: "shared_rw_by_#{user1.username}_to_#{user2.id}",
      privacy: UserTable::PRIVACY_PRIVATE)
    give_permission(table_rw.table_visualization, user2, Carto::Permission::ACCESS_READWRITE)
  end

  def share_group_tables(user, group)
    table_ro = create_table(user_id: user.id, name: "shared_ro_by_#{user.username}_to_#{group.name}", privacy: UserTable::PRIVACY_PRIVATE)
    group.grant_db_permission(table_ro, Carto::Permission::ACCESS_READONLY)
    table_rw = create_table(user_id: user.id, name: "shared_rw_by_#{user.username}_to_#{group.name}", privacy: UserTable::PRIVACY_PRIVATE)
    group.grant_db_permission(table_rw, Carto::Permission::ACCESS_READWRITE)
  end

  def check_tables(moved_user)
    Table.new(user_table: moved_user.tables.where(name: "private_table").first).privacy.should eq UserTable::PRIVACY_PRIVATE
    Table.new(user_table: moved_user.tables.where(name: "public_table").first).privacy.should eq UserTable::PRIVACY_PUBLIC
    Table.new(user_table: moved_user.tables.where(name: "with_link_table").first).privacy.should eq UserTable::PRIVACY_LINK
  end

  def create_user_mover_test_organization
    org = create_organization(name: String.random(5).downcase, quota_in_bytes: 2500.megabytes)

    owner = create_user(quota_in_bytes: 500.megabytes, table_quota: 200, private_tables_enabled: true)
    uo = CartoDB::UserOrganization.new(org.id, owner.id)
    uo.promote_user_to_admin
    owner.db_service.setup_organization_owner
    org.reload
    org
  end

  def give_permission(vis, user, access)
    per = Carto::Permission.find(vis.permission.id)
    per.set_user_permission(user, access)
    per.save
    per.reload
  end

  def run_test_server(port)
    @server_thread = Thread.new do
      Rack::Handler::Thin.run Rails.application, Port: port
    end
  end

  def find_available_port
    server = TCPServer.new('127.0.0.1', 0)
    server.addr[1]
  ensure
    server.close if server
  end
end
