# frozen_string_literal: true

require 'spec_helper'

describe Gitlab::Geo, :geo, :request_store do
  using RSpec::Parameterized::TableSyntax
  include ::EE::GeoHelpers

  let_it_be(:primary_node)   { create(:geo_node, :primary) }
  let_it_be(:secondary_node) { create(:geo_node) }

  shared_examples 'a Geo cached value' do |method, key|
    it 'includes GitLab version and Rails.version in the cache key' do
      expanded_key = "geo:#{key}:#{Gitlab::VERSION}:#{Rails.version}"

      expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:write)
        .with(expanded_key, an_instance_of(String), expires_in: 1.minute).and_call_original
      expect(Rails.cache).to receive(:write)
        .with(expanded_key, an_instance_of(String), expires_in: 2.minutes)

      described_class.public_send(method)
    end
  end

  describe '.current_node' do
    it 'returns a GeoNode instance' do
      expect(GeoNode).to receive(:current_node).and_return(primary_node)

      expect(described_class.current_node).to eq(primary_node)
    end

    it_behaves_like 'a Geo cached value', :current_node, :current_node
  end

  describe '.primary_node' do
    it 'returns a GeoNode primary instance' do
      expect(described_class.primary_node).to eq(primary_node)
    end

    it_behaves_like 'a Geo cached value', :primary_node, :primary_node
  end

  describe '.secondary_nodes' do
    it 'returns a list of Geo secondary nodes' do
      expect(described_class.secondary_nodes).to match_array(secondary_node)
    end

    it_behaves_like 'a Geo cached value', :secondary_nodes, :secondary_nodes
  end

  describe '.primary?' do
    context 'when current node is a primary node' do
      before do
        stub_current_geo_node(primary_node)
      end

      it 'returns true' do
        expect(described_class.primary?).to be_truthy
      end

      it 'returns false when GeoNode is disabled' do
        allow(described_class).to receive(:enabled?) { false }

        expect(described_class.primary?).to be_falsey
      end
    end
  end

  describe '.primary_node_configured?' do
    context 'when current node is a primary node' do
      it 'returns true' do
        expect(described_class.primary_node_configured?).to be_truthy
      end

      it 'returns false when primary does not exist' do
        primary_node.destroy

        expect(described_class.primary_node_configured?).to be_falsey
      end
    end
  end

  describe '.current_node_misconfigured?' do
    it 'returns true when current node is not set' do
      expect(described_class.current_node_misconfigured?).to be_truthy
    end

    it 'returns false when primary' do
      stub_current_geo_node(primary_node)

      expect(described_class.current_node_misconfigured?).to be_falsey
    end

    it 'returns false when secondary' do
      stub_current_geo_node(secondary_node)

      expect(described_class.current_node_misconfigured?).to be_falsey
    end

    it 'returns false when Geo is disabled' do
      GeoNode.delete_all

      expect(described_class.current_node_misconfigured?).to be_falsey
    end
  end

  describe '.secondary?' do
    context 'when current node is a secondary node' do
      before do
        stub_current_geo_node(secondary_node)
      end

      it 'returns true' do
        expect(described_class.secondary?).to be_truthy
      end

      it 'returns false when GeoNode is disabled' do
        allow(described_class).to receive(:enabled?) { false }

        expect(described_class.secondary?).to be_falsey
      end
    end
  end

  describe '.enabled?' do
    it_behaves_like 'a Geo cached value', :enabled?, :node_enabled

    context 'when any GeoNode exists' do
      it 'returns true' do
        expect(described_class.enabled?).to be_truthy
      end
    end

    context 'when no GeoNode exists' do
      before do
        GeoNode.delete_all
      end

      it 'returns false' do
        expect(described_class.enabled?).to be_falsey
      end
    end
  end

  describe '.oauth_authentication' do
    before do
      stub_secondary_node
      stub_current_geo_node(secondary_node)
    end

    it_behaves_like 'a Geo cached value', :oauth_authentication, :oauth_application
  end

  describe '.connected?' do
    context 'when there is a database issue' do
      it 'returns false when database connection is down' do
        allow(GeoNode).to receive(:connected?) { false }

        expect(described_class.connected?).to be_falsey
      end

      it 'returns false when the table does not exist' do
        allow(GeoNode).to receive(:table_exists?) { false }

        expect(described_class.connected?).to be_falsey
      end
    end
  end

  describe '.secondary?' do
    context 'when current node is secondary' do
      it 'returns true' do
        stub_current_geo_node(secondary_node)
        expect(described_class.secondary?).to be_truthy
      end
    end

    context 'current node is primary' do
      it 'returns false' do
        expect(described_class.secondary?).to be_falsey
      end
    end
  end

  describe '.expire_cache!' do
    it 'clears the Geo cache keys', :request_store do
      described_class::CACHE_KEYS.each do |key|
        content = "#{key}-content"

        described_class.cache_value(key) { content }
        expect(described_class.cache_value(key)).to eq(content)
      end

      described_class.expire_cache!

      described_class::CACHE_KEYS.each do |key|
        expect(described_class.cache_value(key) { nil }).to be_nil
      end
    end
  end

  describe '.expire_cache_keys!' do
    it 'clears specified keys', :request_store do
      cache_data = { one: 1, two: 2 }

      cache_data.each do |key, value|
        described_class.cache_value(key) { value }
        expect(described_class.cache_value(key)).to eq(value)
      end

      described_class.expire_cache_keys!(cache_data.keys)

      cache_data.keys.each do |key|
        expect(described_class.cache_value(key) { nil }).to be_nil
      end
    end
  end

  describe '.license_allows?' do
    it 'returns true if license has Geo addon' do
      stub_licensed_features(geo: true)
      expect(described_class.license_allows?).to be_truthy
    end

    it 'returns false if license doesnt have Geo addon' do
      stub_licensed_features(geo: false)
      expect(described_class.license_allows?).to be_falsey
    end

    it 'returns false if no license is present' do
      allow(License).to receive(:current) { nil }
      expect(described_class.license_allows?).to be_falsey
    end
  end

  describe '.generate_access_keys' do
    it 'returns a public and secret access key' do
      keys = described_class.generate_access_keys

      expect(keys[:access_key].length).to eq(20)
      expect(keys[:secret_access_key].length).to eq(40)
    end
  end

  describe '.configure_cron_jobs!' do
    let(:manager) { double('cron_manager').as_null_object }

    before do
      allow(Gitlab::Geo::CronManager).to receive(:new) { manager }
    end

    it 'creates a cron watcher' do
      expect(manager).to receive(:create_watcher!)

      described_class.configure_cron_jobs!
    end

    it 'runs the cron manager' do
      expect(manager).to receive(:execute)

      described_class.configure_cron_jobs!
    end
  end

  describe '.repository_verification_enabled?' do
    context "when the feature flag hasn't been set" do
      it 'returns true' do
        expect(described_class.repository_verification_enabled?).to eq true
      end
    end

    context 'when the feature flag has been set' do
      context 'when the feature flag is set to enabled' do
        it 'returns true' do
          stub_feature_flags(geo_repository_verification: true)

          expect(described_class.repository_verification_enabled?).to eq true
        end
      end

      context 'when the feature flag is set to disabled' do
        it 'returns false' do
          stub_feature_flags(geo_repository_verification: false)

          expect(described_class.repository_verification_enabled?).to eq false
        end
      end
    end
  end

  describe '.allowed_ip?' do
    where(:allowed_ips, :ip, :allowed) do
      "192.1.1.1"                  | "192.1.1.1"     | true
      "192.1.1.1, 192.1.2.1"       | "192.1.2.1"     | true
      "192.1.1.0/24"               | "192.1.1.223"   | true
      "192.1.0.0/16"               | "192.1.223.223" | true
      "192.1.0.0/16, 192.1.2.0/24" | "192.1.2.223"   | true
      "192.1.0.0/16"               | "192.2.1.1"     | false
      "192.1.0.1"                  | "192.2.1.1"     | false
    end

    with_them do
      it do
        stub_application_setting(geo_node_allowed_ips: allowed_ips)

        expect(described_class.allowed_ip?(ip)).to eq(allowed)
      end
    end
  end

  describe '.proxying_to_primary_message' do
    it 'returns a message as a string' do
      url = 'ssh://git@primary.com/namespace/repo.git'
      message = <<~STR
      This request to a Geo secondary node will be forwarded to the
      Geo primary node:

        #{url}
      STR

      expect(described_class.interacting_with_primary_message(url)).to eq(message)
    end
  end

  describe '.redirecting_to_primary_message' do
    it 'returns a message as a string' do
      url = 'http://primary.com/namespace/repo.git'
      message = <<~STR
      This request to a Geo secondary node will be forwarded to the
      Geo primary node:

        #{url}
      STR

      expect(described_class.interacting_with_primary_message(url)).to eq(message)
    end
  end
end