# frozen_string_literal: true

require 'pry'

module Gitlab
  module QA
    describe Component::Gitlab do
      subject(:gitlab_component) { described_class.new }

      around do |example|
        ClimateControl.modify(QA_LOG_PATH: artifacts_dir) { example.run }
      end

      before do
        Runtime::Scenario.define(:omnibus_configuration, Runtime::OmnibusConfiguration.new)
        Runtime::Scenario.define(:seed_db, false)
        Runtime::Scenario.define(:seed_admin_token, true)
        Runtime::Scenario.define(:omnibus_exec_commands, [])
        Runtime::Scenario.define(:skip_server_hooks, true)
      end

      let(:full_ce_address) { 'registry.gitlab.com/foo/gitlab/gitlab-ce' }
      let(:full_ce_address_with_complex_tag) { "#{full_ce_address}:omnibus-7263a2" }
      let(:artifacts_dir) { '/tmp/gitlab-qa/gitlab-qa-run-2018-07-11-10-00-00-abc123' }

      describe '#release' do
        context 'with no release' do
          it 'defaults to CE' do
            expect(gitlab_component.release.to_s).to eq 'gitlab/gitlab-ce:nightly'
          end
        end
      end

      describe '#release=' do
        before do
          gitlab_component.release = release
        end

        context 'when release is a Release object' do
          let(:release) { create_release('CE') }

          it 'returns a correct release' do
            expect(gitlab_component.release.to_s).to eq 'gitlab/gitlab-ce:nightly'
          end
        end

        context 'when release is a string' do
          context 'with a simple tag' do
            let(:release) { full_ce_address_with_complex_tag }

            it 'returns a correct release' do
              expect(gitlab_component.release.to_s).to eq full_ce_address_with_complex_tag
            end
          end
        end
      end

      describe '#name' do
        before do
          gitlab_component.release = create_release('EE')
        end

        it 'returns a unique name' do
          expect(gitlab_component.name).to match(/\Agitlab-ee-(\w+){8}\z/)
        end
      end

      describe '#hostname' do
        it { expect(gitlab_component.hostname).to match(/\Agitlab-ce-(\w+){8}\.\z/) }

        context 'with a network' do
          before do
            gitlab_component.network = 'local'
          end

          it 'returns a valid hostname' do
            expect(gitlab_component.hostname).to match(/\Agitlab-ce-(\w+){8}\.local\z/)
          end
        end
      end

      describe '#address' do
        context 'with a network' do
          before do
            gitlab_component.network = 'local'
          end

          it 'returns a HTTP address' do
            expect(gitlab_component.address)
              .to match(%r{http://gitlab-ce-(\w+){8}\.local\z})
          end
        end
      end

      describe '#start' do
        let(:docker) { spy('docker command') }

        before do
          stub_const('Gitlab::QA::Docker::Command', docker)
          allow(gitlab_component).to receive(:ensure_configured!)
        end

        it 'runs a docker command' do
          gitlab_component.start

          expect(docker).to have_received(:execute!)
        end

        it 'dynamically binds HTTP port' do
          gitlab_component.start

          expect(docker).to have_received(:port).with("80")
        end

        it 'specifies the name' do
          gitlab_component.start

          expect(docker).to have_received(:<<)
                              .with("--name #{gitlab_component.name}")
        end

        it 'specifies the hostname' do
          gitlab_component.start

          expect(docker).to have_received(:<<)
                              .with("--hostname #{gitlab_component.hostname}")
        end

        it 'bind-mounds volume with logs in an appropriate directory' do
          allow(Gitlab::QA::Runtime::Env)
            .to receive(:host_artifacts_dir)
                  .and_return(artifacts_dir)

          gitlab_component.name = 'my-gitlab'

          gitlab_component.start

          expect(docker).to have_received(:volume)
            .with("#{artifacts_dir}/my-gitlab/logs", '/var/log/gitlab', 'Z')
        end

        context 'with a network' do
          before do
            gitlab_component.network = 'testing-network'
          end

          it 'specifies the network' do
            gitlab_component.start

            expect(docker).to have_received(:<<)
                                .with('--net testing-network')
          end
        end

        context 'with volumes' do
          before do
            gitlab_component.volumes = { '/from' => '/to' }
          end

          it 'adds --volume switches to the command' do
            gitlab_component.start

            expect(docker).to have_received(:volume)
                                .with('/from', '/to', 'Z')
          end
        end

        context 'with environment' do
          before do
            gitlab_component.environment = { 'TEST' => 'some value' }
          end

          it 'adds environment variables to the command' do
            gitlab_component.start

            expect(docker).to have_received(:env)
                                .with('TEST', 'some value')
          end
        end

        context 'with network_alias' do
          before do
            gitlab_component.add_network_alias('lolcathost')
          end

          it 'adds --network-alias switches to the command' do
            gitlab_component.start

            expect(docker).to have_received(:<<).with('--network-alias lolcathost')
          end
        end

        describe 'with tls cert volumes' do
          let(:cert_path) { File.expand_path('../../../../tls_certificates', __dir__) }

          let(:alpine_helper) do
            instance_double(
              Gitlab::QA::Component::Alpine,
              :volumes= => nil,
              :start_instance => nil,
              :name => 'alpine',
              :teardown! => nil
            )
          end

          let(:cert_volumes) do
            {
              'authority' => '/etc/gitlab/trusted-certs',
              'gitlab-ssl' => '/etc/gitlab/ssl'
            }
          end

          before do
            allow(Gitlab::QA::Component::Alpine).to receive(:perform).and_yield(alpine_helper)
          end

          it 'creates volumes with tls certs', :aggregate_failures do
            gitlab_component.prepare

            expect(alpine_helper).to have_received(:volumes=).with(cert_volumes)
            expect(alpine_helper).to have_received(:start_instance)
            expect(alpine_helper).to have_received(:teardown!)

            expect(docker).to have_received(:execute)
              .with("cp #{cert_path}/authority/. alpine:#{cert_volumes['authority']}").once
            expect(docker).to have_received(:execute)
              .with("cp #{cert_path}/gitlab/. alpine:#{cert_volumes['gitlab-ssl']}").once
          end
        end
      end

      describe '#seed_db' do
        let(:expect_empty) { false }
        let(:file_patterns) { nil }
        let(:docker_engine) { spy('docker engine') }
        let(:exec_commands) { [] }
        let(:seed_db_dir) do
          dir = File.expand_path("/tmp/gitlab-qa-gitlab-rb-spec-#{SecureRandom.hex(10)}", __dir__)
          FileUtils.mkdir_p(dir)
          FileUtils.touch("#{dir}/test_file1.rb")
          FileUtils.touch("#{dir}/test_file2.rb")
          FileUtils.touch("#{dir}/file3.rb")
          dir
        end

        before do
          stub_const('Gitlab::QA::Docker::Engine', docker_engine)
          stub_const('Gitlab::QA::Component::Gitlab::DATA_SEED_PATH', seed_db_dir)
          stub_const('Gitlab::QA::Component::Gitlab::DATA_PATH', '/d/e/f')

          gitlab_component.instance_variable_set(:@exec_commands, exec_commands)
          gitlab_component.instance_variable_set(:@seed_admin_token, false)
          gitlab_component.instance_variable_set(:@seed_db, seed_db)
          allow(Runtime::Scenario).to receive(:seed_db).and_return(file_patterns)

          gitlab_component.process_exec_commands
        end

        context 'when seed_db is true' do
          let(:seed_db) { true }

          shared_examples 'exec docker commands when instance is ready' do
            it 'copies the data seed path to data path' do
              expect(docker_engine).to have_received(:copy).with(gitlab_component.name, seed_db_dir, '/d/e/f')
            end

            it 'adds the seed test data command to the exec_commands' do
              expected_commands = gitlab_component.send(:seed_test_data_command)
              expect(expected_commands).not_to be_empty unless expect_empty

              expect(exec_commands).to include(expected_commands)
            end
          end

          context 'with duplicated search pattern' do
            let(:file_patterns) { %w[test*.rb test_file1.rb] }

            it_behaves_like 'exec docker commands when instance is ready'
          end

          context 'with all seed scripts' do
            let(:file_patterns) { ['*'] }

            it_behaves_like 'exec docker commands when instance is ready'
          end

          context 'without matches' do
            let(:file_patterns) { ['test_file1'] }
            let(:expect_empty) { true }

            it_behaves_like 'exec docker commands when instance is ready'
          end
        end

        context 'when `--seed-db` is not set' do
          let(:seed_db) { false }
          let(:file_patterns) { ['file3.rb'] }

          it 'does not execute the seed_test_data script' do
            expect(gitlab_component).not_to receive(:seed_test_data_command)
          end
        end
      end

      describe '#seed_admin_token' do
        let(:docker_engine) { spy('docker engine') }
        let(:exec_commands) { [] }

        before do
          stub_const('Gitlab::QA::Docker::Engine', docker_engine)
          stub_const('Gitlab::QA::Component::Gitlab::DATA_SEED_PATH', '/a/b/c')
          stub_const('Gitlab::QA::Component::Gitlab::DATA_PATH', '/d/e/f')
          gitlab_component.instance_variable_set(:@seed_admin_token, seed_admin_token)
          gitlab_component.instance_variable_set(:@exec_commands, exec_commands)
          gitlab_component.process_exec_commands
        end

        context 'when seed_admin_token is true' do
          let(:seed_admin_token) { true }

          it 'copies the data seed path to data pat' do
            expect(docker_engine).to have_received(:copy).with(gitlab_component.name, '/a/b/c', '/d/e/f')
          end

          it 'adds the seed admin token command to the exec_commands' do
            expect(exec_commands).to include(gitlab_component.send(:seed_admin_token_command))
          end
        end

        context 'when seed_admin_token is false' do
          let(:seed_admin_token) { false }

          it 'does not copy the data seed path to data path' do
            expect(docker_engine).not_to have_received(:copy)
          end

          it 'does not add the seed admin token command to the exec_commands' do
            expect(exec_commands).not_to include(gitlab_component.send(:seed_admin_token_command))
          end
        end
      end

      describe '#teardown' do
        let(:docker_engine) { spy('docker engine') }

        before do
          stub_const('Gitlab::QA::Docker::Engine', docker_engine)
        end

        context 'when `--no-teardown` is set' do
          it 'leaves containers running' do
            allow(gitlab_component).to receive(:teardown?).and_return(false)

            gitlab_component.teardown

            expect(docker_engine).not_to have_received(:stop)
            expect(docker_engine).not_to have_received(:remove)
            expect(docker_engine).to have_received(:ps)
          end
        end

        context 'when `--no-teardown` is not set' do
          it 'stops and removes containers' do
            allow(gitlab_component).to receive(:teardown?).and_return(true)
            allow(Gitlab::QA::Runtime::Env).to receive(:host_artifacts_dir).and_return(artifacts_dir)
            file_double = instance_double(File)
            allow(file_double).to receive(:sync=)

            allow(File).to receive(:open).with("#{artifacts_dir}/pg_stats.log", "a").and_return(file_double)
            allow(File).to receive(:open).with("qa/log/path/gitlab-qa.log", 9).and_return(file_double)

            gitlab_component.teardown

            expect(docker_engine).to have_received(:remove)
            expect(docker_engine).not_to have_received(:ps)
          end
        end
      end

      describe '#server_hooks' do
        let(:docker_engine) { spy('docker engine') }

        before do
          stub_const('Gitlab::QA::Docker::Engine', docker_engine)
          gitlab_component.instance_variable_set(:@skip_server_hooks, skip_server_hooks)
        end

        context 'when skip_server_hooks is false' do
          let(:skip_server_hooks) { false }

          it 'adds git server hooks to exec_commands' do
            expect(Support::ConfigScripts).to receive(:add_git_server_hooks).with(docker_engine, gitlab_component.name)
            gitlab_component.process_exec_commands
          end
        end

        context 'when skip_server_hooks is true' do
          let(:skip_server_hooks) { true }

          it 'does not add git server hooks to exec_commands' do
            expect(Support::ConfigScripts).not_to receive(:add_git_server_hooks)
            gitlab_component.process_exec_commands
          end
        end
      end

      describe '#reconfigure' do
        let(:docker) { spy('docker') }

        context 'with default omnibus configuration' do
          before do
            stub_const('Gitlab::QA::Support::ShellCommand', docker)
            allow(Gitlab::QA::Runtime::Env).to receive(:host_artifacts_dir).and_return(artifacts_dir)
            allow(File).to receive(:exist?)
              .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log").and_return(false)
            allow(File).to receive(:open)
              .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log", "w")
            Runtime::Scenario.define(:omnibus_configuration, Runtime::OmnibusConfiguration.new)
          end

          it 'configures omnibus by writing gitlab.rb' do
            gitlab_component.reconfigure
            cfg = Runtime::Scenario.omnibus_configuration.to_s.gsub('"', '\\"')
            expect(docker).to have_received(:new).with(
              eq("docker exec #{gitlab_component.name} bash -c \"echo \\\"#{cfg}\\\" > /etc/gitlab/gitlab.rb;\""),
              anything
            )
          end
        end

        context 'with secrets to mask' do
          before do
            stub_const('Gitlab::QA::Docker::Engine', docker)
            allow(Gitlab::QA::Runtime::Env).to receive(:host_artifacts_dir).and_return(artifacts_dir)
            allow(File).to receive(:exist?)
              .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log").and_return(false)
            allow(File).to receive(:open)
               .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log", "w")
            gitlab_component.secrets = ['secret']
          end

          it 'passes secrets to docker engine' do
            gitlab_component.reconfigure
            expect(docker).to have_received(:write_files).with(gitlab_component.name, { mask_secrets: ['secret'] })
          end
        end

        context 'without secrets to mask' do
          before do
            stub_const('Gitlab::QA::Docker::Engine', docker)
            allow(Gitlab::QA::Runtime::Env).to receive(:host_artifacts_dir).and_return(artifacts_dir)
            allow(File).to receive(:exist?)
              .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log").and_return(false)
            allow(File).to receive(:open)
               .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log", "w")
          end

          it 'does not pass any secrets to docker engine' do
            gitlab_component.reconfigure
            expect(docker).to have_received(:write_files).with(gitlab_component.name, { mask_secrets: [] })
          end
        end

        describe 'log file' do
          let(:docker) { spy('docker') }

          before do
            stub_const('Gitlab::QA::Docker::Engine', docker)
            allow(Gitlab::QA::Runtime::Env).to receive(:host_artifacts_dir).and_return(artifacts_dir)
          end

          context 'when no log file is present' do
            before do
              allow(File).to receive(:exist?)
                .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log").and_return(false)
            end

            it 'writes to the log file' do
              expect(File).to receive(:open).with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log", "w")
              gitlab_component.reconfigure
            end
          end

          context 'with retries' do
            shared_examples 'creates a retry log file' do
              it 'creates a retry log file' do
                expect(File).to receive(:open).with(log_file, "w")
                gitlab_component.reconfigure
              end
            end

            context 'with the first retry' do
              let(:log_file) { "#{artifacts_dir}/#{gitlab_component.name}-retry-1-reconfigure.log" }

              before do
                allow(File).to receive(:exist?)
                  .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log").and_return(true)
                allow(File).to receive(:exist?)
                  .with("#{artifacts_dir}/#{gitlab_component.name}-retry-1-reconfigure.log").and_return(false)
              end

              it_behaves_like 'creates a retry log file'
            end

            context 'with the second retry' do
              let(:log_file) { "#{artifacts_dir}/#{gitlab_component.name}-retry-2-reconfigure.log" }

              before do
                allow(File).to receive(:exist?)
                  .with("#{artifacts_dir}/#{gitlab_component.name}-reconfigure.log").and_return(true)
                allow(File).to receive(:exist?)
                  .with("#{artifacts_dir}/#{gitlab_component.name}-retry-1-reconfigure.log").and_return(true)
                allow(File).to receive(:exist?)
                  .with("#{artifacts_dir}/#{gitlab_component.name}-retry-2-reconfigure.log").and_return(false)
              end

              it_behaves_like 'creates a retry log file'
            end
          end
        end
      end

      describe '#create_key_file' do
        let(:docker) { spy('docker') }

        around do |example|
          ClimateControl.modify(MY_KEY: 'key') { example.run }
        end

        it 'copies a key file' do
          file_path = gitlab_component.create_key_file('MY_KEY')

          stub_const('Gitlab::QA::Docker::Command', docker)
          allow(gitlab_component).to receive(:ensure_configured!)

          gitlab_component.start

          expect(docker).to have_received(:volume)
                                .with(file_path, file_path, 'Z')
        end
      end

      describe '#package_version' do
        it 'returns locked version' do
          allow(gitlab_component).to receive(:read_package_manifest)
            .and_return('{"software":{"package-scripts":{"locked_version":"15.1.2"}}}')

          expect(gitlab_component.package_version).to eq('15.1.2')
        end
      end

      describe '#exist' do
        let(:docker) { spy('docker') }

        it 'calls and returns docker exist true' do
          stub_const('Gitlab::QA::Docker::Engine', docker)

          expect(docker).to receive(:manifest_exists?).with('foo/bar:xyz').and_return(true)

          expect(gitlab_component.exist?('foo/bar', 'xyz')).to be(true)
        end

        it 'calls and returns docker exist false' do
          stub_const('Gitlab::QA::Docker::Engine', docker)

          expect(docker).to receive(:manifest_exists?).with('bar/foo:xyz').and_return(false)

          expect(gitlab_component.exist?('bar/foo', 'xyz')).to be(false)
        end
      end

      describe '#process_exec_commands' do
        let(:docker) { spy('docker') }
        let(:secret) { 'user-agent-secret' }
        let(:command) { "command with secret: #{secret}" }

        before do
          stub_const('Gitlab::QA::Docker::Engine', docker)
          allow(Runtime::Scenario).to receive_messages(omnibus_exec_commands: [command], seed_admin_token: false)
        end

        context 'with secrets to mask' do
          it 'passes secrets to docker engine' do
            gitlab_component.secrets = [secret]
            gitlab_component.process_exec_commands

            expect(docker).to have_received(:exec).with(gitlab_component.name, command, { mask_secrets: [secret] })
          end
        end

        context 'without secrets to mask' do
          it 'does not pass any secrets to docker engine' do
            gitlab_component.process_exec_commands

            expect(docker).to have_received(:exec).with(gitlab_component.name, command, { mask_secrets: [] })
          end
        end
      end

      describe '#set_qa_user_agent' do
        around do |example|
          ClimateControl.modify(GITLAB_QA_USER_AGENT: 'user-agent-secret') { example.run }
        end

        it 'sets GITLAB_QA_USER_AGENT as a rails env var' do
          gitlab_component.set_qa_user_agent

          expect(gitlab_component.omnibus_gitlab_rails_env)
            .to include({ 'GITLAB_QA_USER_AGENT' => 'user-agent-secret' })
        end

        it 'treats the value of GITLAB_QA_USER_AGENT as a secret' do
          gitlab_component.set_qa_user_agent

          expect(gitlab_component.secrets).to include('user-agent-secret')
        end
      end

      private

      def create_release(release)
        Release.new(release)
      end
    end
  end
end
