# frozen_string_literal: true

require 'spec_helper'

describe 'SRV Monitoring' do
  clean_slate_for_all
  require_external_connectivity
  require_mri

  context 'with SRV lookups mocked at Resolver' do
    let(:srv_result) do
      double('srv result').tap do |result|
        allow(result).to receive(:empty?).and_return(false)
        allow(result).to receive(:address_strs).and_return(
          [ ClusterConfig.instance.primary_address_str ]
        )
      end
    end

    let(:client) do
      allow_any_instance_of(Mongo::Srv::Resolver).to receive(:get_records).and_return(srv_result)
      allow_any_instance_of(Mongo::Srv::Resolver).to receive(:get_txt_options_string)

      new_local_client_nmio('mongodb+srv://foo.a.b', server_selection_timeout: 3.15)
    end

    context 'standalone/replica set' do
      require_topology :single, :replica_set

      it 'does not create SRV monitor' do
        expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Unknown)

        client.cluster.run_sdam_flow(
          Mongo::Server::Description.new(ClusterConfig.instance.primary_address_str),
          ClusterConfig.instance.primary_description
        )

        expect(client.cluster.topology).not_to be_a(Mongo::Cluster::Topology::Unknown)

        expect(client.cluster.instance_variable_get(:@srv_monitor)).to be_nil
      end
    end

    context 'sharded cluster' do
      require_topology :sharded

      it 'creates SRV monitor' do
        expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Unknown)

        # Since we force the cluster to run sdam flow which creates a monitor,
        # we need to manually adjust its state.
        client.cluster.instance_variable_set(:@connecting, true)

        client.cluster.run_sdam_flow(
          Mongo::Server::Description.new(ClusterConfig.instance.primary_address_str),
          ClusterConfig.instance.primary_description
        )

        expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Sharded)

        expect(client.cluster.instance_variable_get(:@srv_monitor)).to be_a(Mongo::Srv::Monitor)

        # Close the client in the test rather than allowing our post-test cleanup
        # to take care of it, since the client references test doubles.
        client.close
      end
    end
  end

  # These tests require a sharded cluster to be launched on localhost:27017
  # and localhost:27018, plus internet connectivity for SRV record lookups.
  context 'end to end' do
    require_default_port_deployment

    # JRuby apparently does not implement non-blocking UDP I/O which is used
    # by RubyDNS:
    # NotImplementedError: recvmsg_nonblock is not implemented
    fails_on_jruby

    minimum_mri_version '3.0.0'

    around do |example|
      # Speed up the tests by listening on the fake ports we are using.
      done = false

      servers = []
      threads = [ 27_998, 27_999 ].map do |port|
        Thread.new do
          server = TCPServer.open(port)
          servers << server
          begin
            loop do
              break if done

              begin
                server.accept.close
              rescue StandardError
                nil
              end
            end
          ensure
            server.close
          end
        end
      end

      begin
        example.run
      ensure
        done = true
        servers.map(&:close)

        threads.map(&:kill)
        threads.map(&:join)
      end
    end

    let(:uri) do
      "mongodb+srv://test-fake.test.build.10gen.cc/?tls=#{SpecConfig.instance.ssl?}&tlsInsecure=true"
    end

    let(:logger) do
      Logger.new(STDERR, level: Logger::DEBUG)
    end

    let(:client) do
      new_local_client(uri,
                       SpecConfig.instance.monitoring_options.merge(
                         server_selection_timeout: 3.16,
                         socket_timeout: 8.11,
                         connect_timeout: 8.12,
                         resolv_options: {
                           # Using localhost instead of 127.0.0.1 here causes Ruby's resolv
                           # client to drop responses.
                           nameserver: '127.0.0.1',
                           # TODO: figure out why the address & port here need to be given
                           # twice - if given once, DNS resolution fails.
                           nameserver_port: [ [ '127.0.0.1', 5300 ], [ '127.0.0.1', 5300 ] ],
                         },
                         logger: logger,
                         populator_io: false
                       ))
    end

    before do
      # Expedite the polling process
      allow_any_instance_of(Mongo::Srv::Monitor).to receive(:scan_interval).and_return(1)
    end

    context 'sharded cluster' do
      require_topology :sharded
      require_multi_mongos

      it 'updates topology via SRV records' do
        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_017, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          client.cluster.next_primary
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Sharded)

          address_strs = client.cluster.servers.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27017
                                     ])
        end

        # In Evergreen there are replica set nodes on the next port number
        # after mongos nodes, therefore the addresses in DNS need to accurately
        # reflect how many mongos we have.

        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_018, 'localhost.test.build.10gen.cc' ],
            [ 0, 0, 27_017, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27017
              localhost.test.build.10gen.cc:27018
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27017
                                       localhost.test.build.10gen.cc:27018
                                     ])
        end

        # And because we have only two mongos in Evergreen, test removal
        # separately here.

        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_018, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27018
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27018
                                     ])

          expect(client.cluster.srv_monitor).to be_running
        end
      end
    end

    context 'unknown topology' do
      it 'updates topology via SRV records' do
        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_999, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Unknown)

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27999
                                     ])
        end

        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_998, 'localhost.test.build.10gen.cc' ],
            [ 0, 0, 27_999, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27998
              localhost.test.build.10gen.cc:27999
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27998
                                       localhost.test.build.10gen.cc:27999
                                     ])
        end

        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_997, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27997
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27997
                                     ])

          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Unknown)

          expect(client.cluster.srv_monitor).to be_running
        end
      end
    end

    context 'unknown to sharded' do
      require_topology :sharded

      it 'updates topology via SRV records' do
        skip 'https://jira.mongodb.org/browse/RUBY-3749'
        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_999, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Unknown)

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27999
                                     ])
        end

        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_017, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27017
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27017
                                     ])
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Sharded)

          expect(client.cluster.srv_monitor).to be_running
        end
      end
    end

    context 'unknown to replica set' do
      require_topology :replica_set

      it 'updates topology via SRV records then stops SRV monitor' do
        skip 'https://jira.mongodb.org/browse/RUBY-3749'
        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_999, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::Unknown)

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27999
                                     ])
        end

        rules = [
          [ '_mongodb._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_017, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27017
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers.map(&:address).map(&:seed).sort
          # The actual address will be localhost:27017 or 127.0.0.1:27017,
          # depending on how the replica set is configured.
          expect(address_strs.any? { |str| str =~ /27017/ }).to be true
          # Covers both NoPrimary and WithPrimary replica sets
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::ReplicaSetNoPrimary)

          # give the thread another moment to stop
          sleep 0.1
          expect(client.cluster.srv_monitor).not_to be_running
        end
      end
    end

    context 'when the client mocks the srvServiceName' do
      let(:uri) do
        "mongodb+srv://test-fake.test.build.10gen.cc/?tls=#{SpecConfig.instance.ssl?}&tlsInsecure=true&srvServiceName=customname"
      end

      it 'finds the records using the custom service name' do
        rules = [
          [ '_customname._tcp.test-fake.test.build.10gen.cc', :srv,
            [ 0, 0, 27_998, 'localhost.test.build.10gen.cc' ],
            [ 0, 0, 27_999, 'localhost.test.build.10gen.cc' ], ],
        ]

        mock_dns(rules) do
          15.times do
            address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
            if address_strs == %w[
              localhost.test.build.10gen.cc:27998
              localhost.test.build.10gen.cc:27999
            ]
              break
            end

            sleep 1
          end

          address_strs = client.cluster.servers_list.map(&:address).map(&:seed).sort
          expect(address_strs).to eq(%w[
                                       localhost.test.build.10gen.cc:27998
                                       localhost.test.build.10gen.cc:27999
                                     ])
        end
      end
    end
  end
end
