# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
#
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
#
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

require_relative '../../spec_helper'

describe OpenSearch::Transport::Transport::Sniffer do
  let(:transport) do
    double('transport').tap do |t|
      allow(t).to receive(:perform_request).and_return(response)
      allow(t).to receive(:options).and_return(sniffer_timeout: 2)
    end
  end

  let(:sniffer) do
    described_class.new(transport)
  end

  let(:response) do
    double('response').tap do |r|
      allow(r).to receive(:body).and_return(raw_response)
    end
  end

  let(:raw_response) do
    { 'nodes' => { 'n1' => { 'http' => { 'publish_address' => publish_address } } } }
  end

  let(:publish_address) do
    '127.0.0.1:9250'
  end

  describe '#initialize' do
    it 'has a transport instance' do
      expect(sniffer.transport).to be(transport)
    end

    it 'inherits the sniffer timeout from the transport object' do
      expect(sniffer.timeout).to eq(2)
    end
  end

  describe '#timeout' do
    let(:sniffer) do
      described_class.new(double('transport', options: {}))
    end

    before do
      sniffer.timeout = 3
    end

    it 'allows the timeout to be configured' do
      expect(sniffer.timeout).to eq(3)
    end
  end

  describe '#hosts' do
    let(:hosts) do
      sniffer.hosts
    end

    context 'when the entire response is parsed' do
      let(:raw_response) do
        {
          "cluster_name" => "opensearch_test",
          "nodes" => {
              "N1" => {
                "name" => "Node 1",
                "transport_address" => "127.0.0.1:9300",
                "host" => "testhost1",
                "ip"   => "127.0.0.1",
                "version" => "7.0.0",
                "roles" => [
                  "cluster_manager",
                  "data",
                  "ingest"
                ],
                "attributes" => {
                  "testattr" => "test"
                },
                "http" => {
                  "bound_address" => [
                    "[fe80::1]:9250",
                    "[::1]:9250",
                    "127.0.0.1:9250"
                  ],
                "publish_address" => "127.0.0.1:9250",
                "max_content_length_in_bytes" => 104857600
                }
              }
          }
        }
      end

      it 'parses the id' do
        expect(sniffer.hosts[0][:id]).to eq('N1')
      end

      it 'parses the name' do
        expect(sniffer.hosts[0][:name]).to eq('Node 1')
      end

      it 'parses the version' do
        expect(sniffer.hosts[0][:version]).to eq('7.0.0')
      end

      it 'parses the host' do
        expect(sniffer.hosts[0][:host]).to eq('127.0.0.1')
      end

      it 'parses the port' do
        expect(sniffer.hosts[0][:port]).to eq('9250')
      end

      it 'parses the roles' do
        expect(sniffer.hosts[0][:roles]).to eq(['cluster_manager',
                                                'data',
                                                'ingest'])
      end

      it 'parses the attributes' do
        expect(sniffer.hosts[0][:attributes]).to eq('testattr' => 'test')
      end
    end

    context 'when the transport protocol does not match' do
      let(:raw_response) do
        { 'nodes' => { 'n1' => { 'foo' => { 'publish_address' => '127.0.0.1:9250' } } } }
      end

      it 'does not parse the addresses' do
        expect(hosts).to eq([])
      end
    end

    context 'when a list of nodes is returned' do
      let(:raw_response) do
        { 'nodes' => { 'n1' => { 'http' => { 'publish_address' => '127.0.0.1:9250' } },
                       'n2' => { 'http' => { 'publish_address' => '127.0.0.1:9251' } } } }
      end

      it 'parses the response' do
        expect(hosts.size).to eq(2)
      end

      it 'correctly parses the hosts' do
        expect(hosts[0][:host]).to eq('127.0.0.1')
        expect(hosts[1][:host]).to eq('127.0.0.1')
      end

      it 'correctly parses the ports' do
        expect(hosts[0][:port]).to eq('9250')
        expect(hosts[1][:port]).to eq('9251')
      end
    end

    context 'when the host and port are an ip address and port' do
      it 'parses the response' do
        expect(hosts.size).to eq(1)
      end

      it 'correctly parses the host' do
        expect(hosts[0][:host]).to eq('127.0.0.1')
      end

      it 'correctly parses the port' do
        expect(hosts[0][:port]).to eq('9250')
      end
    end

    context 'when the host and port are a hostname and port' do
      let(:publish_address) do
        'testhost1.com:9250'
      end

      let(:hosts) do
        sniffer.hosts
      end

      it 'parses the response' do
        expect(hosts.size).to eq(1)
      end

      it 'correctly parses the host' do
        expect(hosts[0][:host]).to eq('testhost1.com')
      end

      it 'correctly parses the port' do
        expect(hosts[0][:port]).to eq('9250')
      end
    end

    context 'when the host and port are in the format: hostname/ip:port' do
      let(:publish_address) do
        'example.com/127.0.0.1:9250'
      end

      it 'parses the response' do
        expect(hosts.size).to eq(1)
      end

      it 'uses the hostname' do
        expect(hosts[0][:host]).to eq('example.com')
      end

      it 'correctly parses the port' do
        expect(hosts[0][:port]).to eq('9250')
      end

      context 'when the address is IPv6' do
        let(:publish_address) do
          'example.com/[::1]:9250'
        end

        it 'parses the response' do
          expect(hosts.size).to eq(1)
        end

        it 'uses the hostname' do
          expect(hosts[0][:host]).to eq('example.com')
        end

        it 'correctly parses the port' do
          expect(hosts[0][:port]).to eq('9250')
        end
      end
    end

    context 'when the address is IPv6' do
      let(:publish_address) do
        '[::1]:9250'
      end

      it 'parses the response' do
        expect(hosts.size).to eq(1)
      end

      it 'correctly parses the host' do
        expect(hosts[0][:host]).to eq('::1')
      end

      it 'correctly parses the port' do
        expect(hosts[0][:port]).to eq('9250')
      end
    end

    context 'when the transport has :randomize_hosts option' do
      let(:raw_response) do
        { 'nodes' => { 'n1' => { 'http' => { 'publish_address' => '127.0.0.1:9250' } },
                       'n2' => { 'http' => { 'publish_address' => '127.0.0.1:9251' } } } }
      end

      before do
        allow(transport).to receive(:options).and_return(randomize_hosts: true)
      end

      it 'shuffles the list' do
        expect(hosts.size).to eq(2)
      end
    end
  end
end
