require 'spec_helper'
require 'cloud_controller/seeds'

module VCAP::CloudController
  RSpec.describe VCAP::CloudController::Seeds do
    let(:config) { TestConfig.config_instance.clone }

    describe '.create_seed_stacks' do
      it 'populates stacks' do
        expect(Stack).to receive(:populate)
        Seeds.create_seed_stacks
      end
    end

    describe '.create_seed_shared_isolation_segment' do
      before do
        IsolationSegmentModel.dataset.destroy
      end

      it 'creates the shared isolation segment' do
        expect do
          Seeds.create_seed_shared_isolation_segment(config)
        end.to change(IsolationSegmentModel, :count).from(0).to(1)

        shared_isolation_segment_model = IsolationSegmentModel.first
        expect(shared_isolation_segment_model.name).to eq('shared')
        expect(shared_isolation_segment_model.guid).to eq(IsolationSegmentModel::SHARED_ISOLATION_SEGMENT_GUID)
      end

      context 'when the shared isolation segment already exists' do
        before do
          Seeds.create_seed_shared_isolation_segment(config)
        end

        context 'and the name does not change' do
          it 'does not update the isolation segment' do
            expect_any_instance_of(IsolationSegmentModel).not_to receive(:update)
            Seeds.create_seed_shared_isolation_segment(config)
          end
        end

        context 'and the name changes' do
          it 'sets the name of the shared segment to the new value' do
            expect do
              Seeds.create_seed_shared_isolation_segment(Config.new({ shared_isolation_segment_name: 'original-name' }))
            end.not_to(change(IsolationSegmentModel, :count))

            shared_isolation_segment_model = IsolationSegmentModel.first
            expect(shared_isolation_segment_model.name).to eq('original-name')
            expect(shared_isolation_segment_model.guid).to eq(IsolationSegmentModel::SHARED_ISOLATION_SEGMENT_GUID)
          end

          context 'and the name is already taken' do
            let(:isolation_segment_model) { IsolationSegmentModel.make }

            # this means that it will fail our deployment. To correct this issue we could
            # redeploy with what the old 'shared' isolation segment name
            it 'raises some kind of error TBD' do
              expect do
                Seeds.create_seed_shared_isolation_segment(Config.new({ shared_isolation_segment_name: isolation_segment_model.name }))
              end.to raise_error(Sequel::ValidationFailed, /must be unique/)
            end
          end
        end
      end
    end

    describe '.create_seed_quota_definitions' do
      let(:config) do
        Config.new({
                     quota_definitions: {
                       'small' => {
                         non_basic_services_allowed: false,
                         total_routes: 10,
                         total_services: 10,
                         memory_limit: 1024,
                         total_reserved_route_ports: 10
                       },
                       'default' => {
                         non_basic_services_allowed: true,
                         total_routes: 1000,
                         total_services: 20,
                         memory_limit: 1_024_000,
                         total_reserved_route_ports: 5
                       }
                     },
                     default_quota_definition: 'default'
                   })
      end

      before do
        Organization.dataset.destroy
        QuotaDefinition.dataset.destroy
      end

      context 'when there are no quota definitions' do
        it 'makes them all' do
          expect do
            Seeds.create_seed_quota_definitions(config)
          end.to change(QuotaDefinition, :count).from(0).to(2)

          small_quota = QuotaDefinition[name: 'small']
          expect(small_quota.non_basic_services_allowed).to be(false)
          expect(small_quota.total_routes).to eq(10)
          expect(small_quota.total_services).to eq(10)
          expect(small_quota.memory_limit).to eq(1024)
          expect(small_quota.total_reserved_route_ports).to eq(10)

          default_quota = QuotaDefinition[name: 'default']
          expect(default_quota.non_basic_services_allowed).to be(true)
          expect(default_quota.total_routes).to eq(1000)
          expect(default_quota.total_services).to eq(20)
          expect(default_quota.memory_limit).to eq(1_024_000)
          expect(default_quota.total_reserved_route_ports).to eq(5)
        end
      end

      context 'when all the quota definitions exist already' do
        before do
          Seeds.create_seed_quota_definitions(config)
        end

        context 'when the existing records exactly match the config' do
          it 'does not create duplicates' do
            expect do
              Seeds.create_seed_quota_definitions(config)
            end.not_to(change(QuotaDefinition, :count))

            small_quota = QuotaDefinition[name: 'small']
            expect(small_quota.non_basic_services_allowed).to be(false)
            expect(small_quota.total_routes).to eq(10)
            expect(small_quota.total_services).to eq(10)
            expect(small_quota.memory_limit).to eq(1024)
            expect(small_quota.total_reserved_route_ports).to eq(10)

            default_quota = QuotaDefinition[name: 'default']
            expect(default_quota.non_basic_services_allowed).to be(true)
            expect(default_quota.total_routes).to eq(1000)
            expect(default_quota.total_services).to eq(20)
            expect(default_quota.memory_limit).to eq(1_024_000)
            expect(default_quota.total_reserved_route_ports).to eq(5)
          end
        end

        context 'when there are records with names that match but other fields that do not match' do
          it 'warns' do
            mock_logger = double
            allow(Steno).to receive(:logger).and_return(mock_logger)
            config.set(:quota_definitions,
                       config.get(:quota_definitions).deep_merge('small' => { total_routes: 2 }))

            expect(mock_logger).to receive(:warn).with('seeds.quota-collision', hash_including(name: 'small'))

            Seeds.create_seed_quota_definitions(config)
          end
        end
      end
    end

    describe '.create_seed_organizations' do
      context 'when system domain organization is missing in the configuration' do
        it 'does not create an organization' do
          config_without_org = config.clone
          config_without_org.set(:system_domain_organization, nil)

          expect do
            Seeds.create_seed_organizations(config_without_org)
          end.not_to(change(Organization, :count))
        end
      end

      context 'when system domain organization exists in the configuration' do
        before do
          Organization.dataset.destroy
          QuotaDefinition.dataset.destroy
        end

        context 'when default quota definition is missing in configuraition' do
          it 'raises error' do
            expect do
              Seeds.create_seed_organizations(config)
            end.to raise_error(ArgumentError, /missing default quota definition in config file/i)
          end
        end

        context 'when default quota definition exists' do
          before do
            QuotaDefinition.make(name: 'default')
          end

          it 'creates the system organization when the organization does not already exist' do
            expect do
              Seeds.create_seed_organizations(config)
            end.to change(Organization, :count).from(0).to(1)

            org = Organization.first
            expect(org.quota_definition.name).to eq('default')
            expect(org.name).to eq('the-system_domain-org-name')
          end

          it 'warns when the system organization exists and has a different quota' do
            Seeds.create_seed_organizations(config)
            org = Organization.find(name: 'the-system_domain-org-name')
            QuotaDefinition.make(name: 'runaway')
            org.quota_definition = QuotaDefinition.find(name: 'runaway')
            org.save(validate: false) # See tracker story #61090364

            mock_logger = double
            allow(Steno).to receive(:logger).and_return(mock_logger)

            expect(mock_logger).to receive(:warn).with('seeds.system-domain-organization.collision', existing_quota_name: 'runaway')

            Seeds.create_seed_organizations(config)
          end
        end
      end
    end

    describe '.create_seed_domains' do
      let(:config) do
        Config.new({
                     app_domains: app_domains,
                     system_domain: system_domain,
                     system_domain_organization: 'the-system-org',
                     quota_definitions: {
                       'default' => {
                         non_basic_services_allowed: true,
                         total_routes: 1000,
                         total_services: 20,
                         memory_limit: 1_024_000
                       }
                     },
                     default_quota_definition: 'default'
                   })
      end
      let(:system_org) { Organization.find(name: 'the-system-org') }
      let(:system_domain) { 'system.example.com' }

      before do
        Domain.dataset.destroy
        Organization.dataset.destroy
        QuotaDefinition.dataset.destroy
        QuotaDefinition.make(name: 'default')
        Seeds.create_seed_organizations(config)
      end

      context 'when the app domains do not include the system domain' do
        let(:app_domains) { ['app.some-other-domain.com'] }

        it 'makes a shared domain for each app domain, and a private domain for the system domain' do
          Seeds.create_seed_domains(config, Organization.find(name: 'the-system-org'))
          expect(Domain.shared_domains.map(&:name)).to eq(['app.some-other-domain.com'])
          expect(Domain.private_domains.map(&:name)).to eq(['system.example.com'])
        end

        it 'raises if the system org is not specified' do
          expect { Seeds.create_seed_domains(config, nil) }.to raise_error(RuntimeError, /system_domain_organization must be provided/)
        end

        it 'creates the system domain if the system domain does not exist' do
          Seeds.create_seed_domains(config, system_org)

          system_domain = Domain.find(name: config.get(:system_domain))
          expect(system_domain.owning_organization).to eq(system_org)
        end

        it 'warns if the system domain exists but has different attributes from the seed' do
          mock_logger = double(info: nil)
          allow(Steno).to receive(:logger).and_return(mock_logger)

          expect(mock_logger).to receive(:warn).with('seeds.system-domain.collision', instance_of(Hash))

          PrivateDomain.create(
            name: config.get(:system_domain),
            owning_organization: Organization.make
          )
          Seeds.create_seed_domains(config, system_org)
        end

        context 'when an app domain is an internal domain' do
          let(:app_domains) { ['app.some-other-domain.com', { 'name' => 'internal.domain.name', 'internal' => true }] }

          it 'creates an internal shared domain' do
            Seeds.create_seed_domains(config, Organization.find(name: 'the-system-org'))
            expect(Domain.shared_domains.map(&:name)).to contain_exactly('app.some-other-domain.com', 'internal.domain.name')
            expect(SharedDomain.first(name: 'internal.domain.name')).to be_internal
          end
        end

        context 'when the app domains include a subdomain of the system domain' do
          let(:app_domains) { ['app.example.com'] }
          let(:system_domain) { 'example.com' }

          it 'adds both as shared domains' do
            Seeds.create_seed_domains(config, Organization.find(name: 'the-system-org'))
            expect(Domain.shared_domains.map(&:name)).to contain_exactly('app.example.com', 'example.com')
            expect(Domain.private_domains.map(&:name)).to eq([])
          end
        end

        context 'when the system domain already exists as a shared domain' do
          let(:app_domains) { ['app.example.com'] }
          let(:system_domain) { 'system.example.com' }

          before do
            SharedDomain.make(name: 'system.example.com')
          end

          it 'that shared domain is not modified' do
            Seeds.create_seed_domains(config, Organization.find(name: 'the-system-org'))
            expect(Domain.shared_domains.map(&:name)).to contain_exactly('app.example.com', 'system.example.com')
            expect(Domain.private_domains.map(&:name)).to eq([])
          end
        end

        context 'when the app domain is one of the system hostnames + system domain' do
          let(:app_domains) { ['uaa.example.com'] }
          let(:system_domain) { 'example.com' }

          before do
            TestConfig.override(system_hostnames: %w[api uaa])
            SharedDomain.make(name: 'example.com')
          end

          it 'returns an error about app domain overlapping with system hostnames' do
            expect { Seeds.create_seed_domains(config, Organization.find(name: 'the-system-org')) }.
              to raise_error(RuntimeError, /App domain cannot overlap with reserved system hostnames/)
          end
        end

        context 'when the app domains include the system domain' do
          let(:app_domains) { ['app.example.com'] }

          before do
            config.set(:app_domains, config.get(:app_domains) + [config.get(:system_domain)])
          end

          it 'makes a shared domain for each app domain, including the system domain' do
            Seeds.create_seed_domains(config, Organization.find(name: 'the-system-org'))
            expect(Domain.shared_domains.map(&:name)).to eq(config.get(:app_domains))
          end
        end

        context 'when a router group name is specified' do
          let(:client) { instance_double(VCAP::CloudController::RoutingApi::Client, enabled?: true) }
          let(:app_domains) { [{ 'name' => 'app.example.com', 'router_group_name' => 'default-tcp' }] }

          before do
            locator = CloudController::DependencyLocator.instance
            allow(locator).to receive(:routing_api_client).and_return(client)
            allow(client).to receive(:router_group_guid).with('default-tcp').and_return('some-router-guid')
          end

          it 'seeds the shared domains with the router group guid' do
            Seeds.create_seed_domains(config, system_org)
            expect(Domain.shared_domains.map(&:name)).to eq(['app.example.com'])
            expect(Domain.shared_domains.map(&:router_group_guid)).to eq(['some-router-guid'])
          end
        end

        context 'when the routing api is initially unavailable and then comes online' do
          let(:app_domains) { [{ 'name' => 'app.example.com', 'router_group_name' => 'default-tcp' }] }

          before do
            allow_any_instance_of(RoutingApi::Client).to receive(:token_info).and_return(OpenStruct.new(auth_header: 'my_token'))
            stub_request(:get, 'http://localhost:3000/routing/v1/router_groups').
              to_return({ status: 404, body: '[]' },
                        { status: 500, body: '[]' },
                        { status: 200, body: '[{"name": "default-tcp", "guid": "some-router-guid"}]' })
          end

          it 'seeds the shared domains with the router group guid' do
            Seeds.create_seed_domains(config, system_org)
            expect(Domain.shared_domains.map(&:name)).to eq(['app.example.com'])
            expect(Domain.shared_domains.map(&:router_group_guid)).to eq(['some-router-guid'])
          end
        end

        context 'when a nonexistent router group name is specified' do
          let(:app_domains) { [{ 'name' => 'app.example.com', 'router_group_name' => 'not-there' }] }
          let(:client) { instance_double(VCAP::CloudController::RoutingApi::Client, enabled?: true) }

          before do
            locator = CloudController::DependencyLocator.instance
            allow(locator).to receive(:routing_api_client).and_return(client)
            allow(client).to receive(:router_group_guid).and_return(nil)
          end

          it 'raises and error' do
            expect do
              Seeds.create_seed_domains(config, system_org)
            end.to raise_error('Unknown router_group_name specified: not-there')
          end
        end

        context 'when routing api is disabled' do
          let(:disabled_client) { RoutingApi::DisabledClient.new }
          let(:app_domains) { [{ 'name' => 'app.example.com', 'router_group_name' => 'default-tcp' }] }

          before do
            locator = CloudController::DependencyLocator.instance
            allow(locator).to receive(:routing_api_client).and_return(disabled_client)
          end

          it 'raises an error' do
            expect do
              Seeds.create_seed_domains(config, system_org)
            end.to raise_error(RoutingApi::RoutingApiDisabled)
          end
        end
      end
    end

    describe '.create_seed_security_groups' do
      let(:config) do
        Config.new({
                     security_group_definitions: [
                       {
                         'name' => 'staging_default',
                         'rules' => []
                       },
                       {
                         'name' => 'running_default',
                         'rules' => []
                       },
                       {
                         'name' => 'non_default',
                         'rules' => []
                       }
                     ],
                     default_staging_security_groups: ['staging_default'],
                     default_running_security_groups: ['running_default']
                   })
      end

      context 'when there are no security groups configured in the system' do
        before do
          SecurityGroup.dataset.destroy
        end

        it 'creates the security groups specified and sets the correct defaults' do
          expect do
            Seeds.create_seed_security_groups(config)
          end.to change(SecurityGroup, :count).by(3)

          staging_def = SecurityGroup.find(name: 'staging_default')
          expect(staging_def.staging_default).to be true
          expect(staging_def.running_default).to be false

          running_def = SecurityGroup.find(name: 'running_default')
          expect(running_def.staging_default).to be false
          expect(running_def.running_default).to be true

          non_def = SecurityGroup.find(name: 'non_default')
          expect(non_def.staging_default).to be false
          expect(non_def.running_default).to be false
        end

        context 'when the staging and running default are the same' do
          before do
            config.set(:default_running_security_groups, 'staging_default')
          end

          it 'creates the security groups specified and sets the correct defaults' do
            expect do
              Seeds.create_seed_security_groups(config)
            end.to change(SecurityGroup, :count).by(3)

            staging_def = SecurityGroup.find(name: 'staging_default')
            expect(staging_def.staging_default).to be true
            expect(staging_def.running_default).to be true

            running_def = SecurityGroup.find(name: 'running_default')
            expect(running_def.staging_default).to be false
            expect(running_def.running_default).to be false

            non_def = SecurityGroup.find(name: 'non_default')
            expect(non_def.staging_default).to be false
            expect(non_def.running_default).to be false
          end
        end

        context 'when there are no default staging and running groups' do
          before do
            config.set(:default_running_security_groups, [])
            config.set(:default_staging_security_groups, [])
          end

          it 'creates the security groups specified and sets the correct defaults' do
            expect do
              Seeds.create_seed_security_groups(config)
            end.to change(SecurityGroup, :count).by(3)

            staging_def = SecurityGroup.find(name: 'staging_default')
            expect(staging_def.staging_default).to be false
            expect(staging_def.running_default).to be false

            running_def = SecurityGroup.find(name: 'running_default')
            expect(running_def.staging_default).to be false
            expect(running_def.running_default).to be false

            non_def = SecurityGroup.find(name: 'non_default')
            expect(non_def.staging_default).to be false
            expect(non_def.running_default).to be false
          end
        end

        context 'when there are more than one default staging and running groups' do
          before do
            config.set(:default_running_security_groups, %w[running_default non_default])
            config.set(:default_staging_security_groups, %w[staging_default non_default])
          end

          it 'creates the security groups specified and sets the correct defaults' do
            expect do
              Seeds.create_seed_security_groups(config)
            end.to change(SecurityGroup, :count).by(3)

            staging_def = SecurityGroup.find(name: 'staging_default')
            expect(staging_def.staging_default).to be true
            expect(staging_def.running_default).to be false

            running_def = SecurityGroup.find(name: 'running_default')
            expect(running_def.staging_default).to be false
            expect(running_def.running_default).to be true

            non_def = SecurityGroup.find(name: 'non_default')
            expect(non_def.staging_default).to be true
            expect(non_def.running_default).to be true
          end
        end

        context 'when no security group seed data is specified in the config' do
          let(:config) do
            Config.new({})
          end

          it 'does nothing' do
            expect do
              Seeds.create_seed_security_groups(config)
            end.not_to(change(SecurityGroup, :count))
          end
        end
      end

      context 'when there are exisiting security groups' do
        before do
          SecurityGroup.make(name: 'EXISTING SECURITY GROUP')
        end

        it 'does nothing' do
          expect do
            Seeds.create_seed_security_groups(config)
          end.not_to(change(SecurityGroup, :count))
        end
      end
    end

    describe '.create_seed_environment_variable_groups' do
      context 'when there are not running and staging environment variable groups' do
        before do
          EnvironmentVariableGroup.dataset.destroy
        end

        it 'creates the running and staging environment variable groups' do
          expect(EnvironmentVariableGroup.find(name: 'running')).to be_nil
          expect(EnvironmentVariableGroup.find(name: 'staging')).to be_nil
          Seeds.create_seed_environment_variable_groups
          expect(EnvironmentVariableGroup.find(name: 'running')).not_to be_nil
          expect(EnvironmentVariableGroup.find(name: 'staging')).not_to be_nil
        end

        context 'if another instance of CC wins a race and creates the group while we are creating the group' do
          it 'continues gracefully when running already exists' do
            allow(EnvironmentVariableGroup).to receive(:running).and_raise(Sequel::UniqueConstraintViolation.new)

            expect do
              Seeds.create_seed_environment_variable_groups
            end.not_to raise_error
          end

          it 'continues gracefully when staging already exists' do
            allow(EnvironmentVariableGroup).to receive(:staging).and_raise(Sequel::UniqueConstraintViolation.new)

            expect do
              Seeds.create_seed_environment_variable_groups
            end.not_to raise_error
          end
        end
      end
    end

    describe '.parsed_domains' do
      context 'when app domain is an array of strings' do
        let(:app_domains) { ['string1.com', 'string2.com'] }

        it 'returns an array of hashes' do
          expected_result = [{ 'name' => 'string1.com' }, { 'name' => 'string2.com' }]
          expect(Seeds.parsed_domains(app_domains)).to eq(expected_result)
        end
      end

      context 'when app domains is an array of hashes' do
        let(:app_domains) do
          [{ 'name' => 'string1.com',
             'router_group_name' => 'some-name' },
           { 'name' => 'string2.com' }]
        end

        it 'returns in the same format' do
          expected_result = [{ 'name' => 'string1.com', 'router_group_name' => 'some-name' },
                             { 'name' => 'string2.com' }]
          expect(Seeds.parsed_domains(app_domains)).to eq(expected_result)
        end
      end
    end

    describe '.seed_encryption_key_sentinels' do
      let(:label1) { 'encryption_key_label_1' }
      let(:label2) { 'encryption_key_label_2' }
      let(:label3) { 'encryption_key_label_3' }

      let(:database_encryption_keys_config) do
        {
          database_encryption: { keys:
            {
              label1.to_sym => 'secret_key1',
              label2.to_sym => 'secret_key2',
              label3.to_sym => 'secret_key3'
            },
                                 current_key_label: label2 }
        }
      end

      context 'when database_encryption keys are present' do
        context 'when the table is empty' do
          let(:config) { Config.new(database_encryption_keys_config) }

          it 'populates the encryption key sentinels' do
            Seeds.seed_encryption_key_sentinels(config)

            label1_sentinel = EncryptionKeySentinelModel.find(encryption_key_label: label1)
            expect(label1_sentinel.encryption_iterations).to eq(Encryptor::ENCRYPTION_ITERATIONS)

            decrypted_value = Encryptor.decrypt_raw(label1_sentinel.encrypted_value, 'secret_key1', label1_sentinel.salt, iterations: label1_sentinel.encryption_iterations)
            expect(decrypted_value).to eq(label1_sentinel.expected_value)

            label2_sentinel = EncryptionKeySentinelModel.find(encryption_key_label: label2)
            decrypted_value = Encryptor.decrypt_raw(label2_sentinel.encrypted_value, 'secret_key2', label2_sentinel.salt, iterations: label2_sentinel.encryption_iterations)
            expect(decrypted_value).to eq(label2_sentinel.expected_value)

            label3_sentinel = EncryptionKeySentinelModel.find(encryption_key_label: label3)
            decrypted_value = Encryptor.decrypt_raw(label3_sentinel.encrypted_value, 'secret_key3', label3_sentinel.salt, iterations: label3_sentinel.encryption_iterations)
            expect(decrypted_value).to eq(label3_sentinel.expected_value)
          end
        end

        context 'when the encryption keys already exist' do
          let(:updated_database_encryption_keys_config) do
            {
              database_encryption: { keys:
                {
                  label1.to_sym => 'new_secret_1',
                  label2.to_sym => 'new_secret_2',
                  label3.to_sym => 'new_secret_3'
                },
                                     current_key_label: label2 }
            }
          end

          let(:config) { Config.new(database_encryption_keys_config) }
          let(:updated_config) { Config.new(updated_database_encryption_keys_config) }

          it 'does not change the existing values' do
            Seeds.seed_encryption_key_sentinels(config)
            Seeds.seed_encryption_key_sentinels(updated_config)

            label1_sentinel = EncryptionKeySentinelModel.find(encryption_key_label: label1)
            expect(label1_sentinel.encryption_iterations).to eq(Encryptor::ENCRYPTION_ITERATIONS)

            decrypted_value = Encryptor.decrypt_raw(label1_sentinel.encrypted_value, 'secret_key1', label1_sentinel.salt, iterations: label1_sentinel.encryption_iterations)
            expect(decrypted_value).to eq(label1_sentinel.expected_value)

            label2_sentinel = EncryptionKeySentinelModel.find(encryption_key_label: label2)
            decrypted_value = Encryptor.decrypt_raw(label2_sentinel.encrypted_value, 'secret_key2', label2_sentinel.salt, iterations: label2_sentinel.encryption_iterations)
            expect(decrypted_value).to eq(label2_sentinel.expected_value)

            label3_sentinel = EncryptionKeySentinelModel.find(encryption_key_label: label3)
            decrypted_value = Encryptor.decrypt_raw(label3_sentinel.encrypted_value, 'secret_key3', label3_sentinel.salt, iterations: label3_sentinel.encryption_iterations)
            expect(decrypted_value).to eq(label3_sentinel.expected_value)
          end
        end
      end

      context 'when database_encryption keys are not present' do
        let(:config) { Config.new({}) }

        it 'does not break' do
          expect do
            Seeds.seed_encryption_key_sentinels(config)
          end.not_to raise_error
        end
      end
    end
  end
end
