// 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.

//go:build !integration
// +build !integration

package elasticsearch

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"errors"
	"github.com/elastic/go-elasticsearch/v8/esapi"
	"github.com/elastic/go-elasticsearch/v8/typedapi/types"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"testing"

	"github.com/elastic/elastic-transport-go/v8/elastictransport"
)

var metaHeaderReValidation = regexp.MustCompile(`^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$`)
var called bool

type mockTransp struct {
	RoundTripFunc func(*http.Request) (*http.Response, error)
}

var defaultRoundTripFunc = func(req *http.Request) (*http.Response, error) {
	response := &http.Response{Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}}}

	if req.URL.Path == "/" {
		response.Body = ioutil.NopCloser(strings.NewReader(`{
		  "version" : {
			"number" : "8.0.0-SNAPSHOT",
			"build_flavor" : "default"
		  },
		  "tagline" : "You Know, for Search"
		}`))
		response.Header.Add("Content-Type", "application/json")
	} else {
		called = true
	}

	return response, nil
}

func (t *mockTransp) RoundTrip(req *http.Request) (*http.Response, error) {
	if t.RoundTripFunc == nil {
		return defaultRoundTripFunc(req)
	}
	return t.RoundTripFunc(req)
}

func TestClientConfiguration(t *testing.T) {
	t.Parallel()

	t.Run("With empty", func(t *testing.T) {
		c, err := NewDefaultClient()

		if err != nil {
			t.Errorf("Unexpected error: %s", err)
		}

		u := c.Transport.(*elastictransport.Client).URLs()[0].String()

		if u != defaultURL {
			t.Errorf("Unexpected URL, want=%s, got=%s", defaultURL, u)
		}
	})

	t.Run("With URL from Addresses", func(t *testing.T) {
		c, err := NewClient(Config{Addresses: []string{"http://localhost:8080//"}})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		u := c.Transport.(*elastictransport.Client).URLs()[0].String()

		if u != "http://localhost:8080" {
			t.Errorf("Unexpected URL, want=http://localhost:8080, got=%s", u)
		}
	})

	t.Run("With URL from environment", func(t *testing.T) {
		os.Setenv("ELASTICSEARCH_URL", "http://example.com")
		defer func() { os.Setenv("ELASTICSEARCH_URL", "") }()

		c, err := NewDefaultClient()
		if err != nil {
			t.Errorf("Unexpected error: %s", err)
		}

		u := c.Transport.(*elastictransport.Client).URLs()[0].String()

		if u != "http://example.com" {
			t.Errorf("Unexpected URL, want=http://example.com, got=%s", u)
		}
	})

	t.Run("With URL from environment and cfg.Addresses", func(t *testing.T) {
		os.Setenv("ELASTICSEARCH_URL", "http://example.com")
		defer func() { os.Setenv("ELASTICSEARCH_URL", "") }()

		c, err := NewClient(Config{Addresses: []string{"http://localhost:8080//"}})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		u := c.Transport.(*elastictransport.Client).URLs()[0].String()

		if u != "http://localhost:8080" {
			t.Errorf("Unexpected URL, want=http://localhost:8080, got=%s", u)
		}
	})

	t.Run("With URL from environment and cfg.CloudID", func(t *testing.T) {
		os.Setenv("ELASTICSEARCH_URL", "http://example.com")
		defer func() { os.Setenv("ELASTICSEARCH_URL", "") }()

		c, err := NewClient(Config{CloudID: "foo:YmFyLmNsb3VkLmVzLmlvJGFiYzEyMyRkZWY0NTY="})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		u := c.Transport.(*elastictransport.Client).URLs()[0].String()

		if u != "https://abc123.bar.cloud.es.io" {
			t.Errorf("Unexpected URL, want=https://abc123.bar.cloud.es.io, got=%s", u)
		}
	})

	t.Run("With cfg.Addresses and cfg.CloudID", func(t *testing.T) {
		_, err := NewClient(Config{Addresses: []string{"http://localhost:8080//"}, CloudID: "foo:ABC="})
		if err == nil {
			t.Fatalf("Expected error, got: %v", err)
		}
		match, _ := regexp.MatchString("both .* are set", err.Error())
		if !match {
			t.Errorf("Expected error when addresses from environment and configuration are used together, got: %v", err)
		}
	})

	t.Run("With CloudID", func(t *testing.T) {
		// bar.cloud.es.io$abc123$def456
		c, err := NewClient(Config{CloudID: "foo:YmFyLmNsb3VkLmVzLmlvJGFiYzEyMyRkZWY0NTY="})
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		u := c.Transport.(*elastictransport.Client).URLs()[0].String()

		if u != "https://abc123.bar.cloud.es.io" {
			t.Errorf("Unexpected URL, want=https://abc123.bar.cloud.es.io, got=%s", u)
		}
	})

	t.Run("With invalid CloudID", func(t *testing.T) {
		var err error

		_, err = NewClient(Config{CloudID: "foo:ZZZ==="})
		if err == nil {
			t.Errorf("Expected error for CloudID, got: %v", err)
		}

		_, err = NewClient(Config{CloudID: "foo:Zm9v"})
		if err == nil {
			t.Errorf("Expected error for CloudID, got: %v", err)
		}

		_, err = NewClient(Config{CloudID: "foo:"})
		if err == nil {
			t.Errorf("Expected error for CloudID, got: %v", err)
		}
	})

	t.Run("With invalid URL", func(t *testing.T) {
		u := ":foo"
		_, err := NewClient(Config{Addresses: []string{u}})

		if err == nil {
			t.Errorf("Expected error for URL %q, got %v", u, err)
		}
	})

	t.Run("With invalid URL from environment", func(t *testing.T) {
		os.Setenv("ELASTICSEARCH_URL", ":foobar")
		defer func() { os.Setenv("ELASTICSEARCH_URL", "") }()

		c, err := NewDefaultClient()
		if err == nil {
			t.Errorf("Expected error, got: %+v", c)
		}
	})
}

func TestClientInterface(t *testing.T) {
	t.Run("Transport", func(t *testing.T) {
		c, err := NewClient(Config{Transport: &mockTransp{}})

		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		if called != false { // megacheck ignore
			t.Errorf("Unexpected call to transport by client")
		}

		c.Perform(&http.Request{URL: &url.URL{}, Header: make(http.Header)}) // errcheck ignore

		if called != true { // megacheck ignore
			t.Errorf("Expected client to call transport")
		}
	})
}

func TestAddrsToURLs(t *testing.T) {
	tt := []struct {
		name  string
		addrs []string
		urls  []*url.URL
		err   error
	}{
		{
			name: "valid",
			addrs: []string{
				"http://example.com",
				"https://example.com",
				"http://192.168.255.255",
				"http://example.com:8080",
			},
			urls: []*url.URL{
				{Scheme: "http", Host: "example.com"},
				{Scheme: "https", Host: "example.com"},
				{Scheme: "http", Host: "192.168.255.255"},
				{Scheme: "http", Host: "example.com:8080"},
			},
			err: nil,
		},
		{
			name:  "trim trailing slash",
			addrs: []string{"http://example.com/", "http://example.com//"},
			urls: []*url.URL{
				{Scheme: "http", Host: "example.com", Path: ""},
				{Scheme: "http", Host: "example.com", Path: ""},
			},
		},
		{
			name:  "keep suffix",
			addrs: []string{"http://example.com/foo"},
			urls:  []*url.URL{{Scheme: "http", Host: "example.com", Path: "/foo"}},
		},
		{
			name:  "invalid url",
			addrs: []string{"://invalid.com"},
			urls:  nil,
			err:   errors.New("missing protocol scheme"),
		},
	}
	for _, tc := range tt {
		t.Run(tc.name, func(t *testing.T) {
			res, err := addrsToURLs(tc.addrs)

			if tc.err != nil {
				if err == nil {
					t.Errorf("Expected error, got: %v", err)
				}
				match, _ := regexp.MatchString(tc.err.Error(), err.Error())
				if !match {
					t.Errorf("Expected err [%s] to match: %s", err.Error(), tc.err.Error())
				}
			}

			for i := range tc.urls {
				if res[i].Scheme != tc.urls[i].Scheme {
					t.Errorf("%s: Unexpected scheme, want=%s, got=%s", tc.name, tc.urls[i].Scheme, res[i].Scheme)
				}
			}
			for i := range tc.urls {
				if res[i].Host != tc.urls[i].Host {
					t.Errorf("%s: Unexpected host, want=%s, got=%s", tc.name, tc.urls[i].Host, res[i].Host)
				}
			}
			for i := range tc.urls {
				if res[i].Path != tc.urls[i].Path {
					t.Errorf("%s: Unexpected path, want=%s, got=%s", tc.name, tc.urls[i].Path, res[i].Path)
				}
			}
		})
	}
}

func TestCloudID(t *testing.T) {
	t.Run("Parse", func(t *testing.T) {
		var testdata = []struct {
			in  string
			out string
		}{
			{
				in:  "name:" + base64.StdEncoding.EncodeToString([]byte("host$es_uuid$kibana_uuid")),
				out: "https://es_uuid.host",
			},
			{
				in:  "name:" + base64.StdEncoding.EncodeToString([]byte("host:9243$es_uuid$kibana_uuid")),
				out: "https://es_uuid.host:9243",
			},
			{
				in:  "name:" + base64.StdEncoding.EncodeToString([]byte("host$es_uuid$")),
				out: "https://es_uuid.host",
			},
			{
				in:  "name:" + base64.StdEncoding.EncodeToString([]byte("host$es_uuid")),
				out: "https://es_uuid.host",
			},
		}

		for _, tt := range testdata {
			actual, err := addrFromCloudID(tt.in)
			if err != nil {
				t.Errorf("Unexpected error: %s", err)
			}
			if actual != tt.out {
				t.Errorf("Unexpected output, want=%q, got=%q", tt.out, actual)
			}
		}

	})

	t.Run("Invalid format", func(t *testing.T) {
		input := "foobar"
		_, err := addrFromCloudID(input)
		if err == nil {
			t.Errorf("Expected error for input %q, got %v", input, err)
		}
		match, _ := regexp.MatchString("unexpected format", err.Error())
		if !match {
			t.Errorf("Unexpected error string: %s", err)
		}
	})

	t.Run("Invalid base64 value", func(t *testing.T) {
		input := "foobar:xxxxx"
		_, err := addrFromCloudID(input)
		if err == nil {
			t.Errorf("Expected error for input %q, got %v", input, err)
		}
		match, _ := regexp.MatchString("illegal base64 data", err.Error())
		if !match {
			t.Errorf("Unexpected error string: %s", err)
		}
	})
}

func TestVersion(t *testing.T) {
	if Version == "" {
		t.Error("Version is empty")
	}
}

func TestClientMetrics(t *testing.T) {
	c, _ := NewClient(Config{EnableMetrics: true})

	m, err := c.Metrics()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if m.Requests != 0 {
		t.Errorf("Unexpected output: %s", m)
	}
}

func TestResponseCheckOnly(t *testing.T) {
	tests := []struct {
		name       string
		response   *http.Response
		requestErr error
		wantErr    bool
	}{
		{
			name: "Valid answer with header",
			response: &http.Response{
				Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
				Body:   ioutil.NopCloser(strings.NewReader("{}")),
			},
			wantErr: false,
		},
		{
			name: "Valid answer without header",
			response: &http.Response{
				StatusCode: http.StatusOK,
				Body:       ioutil.NopCloser(strings.NewReader("{}")),
			},
			wantErr: true,
		},
		{
			name: "Valid answer with header and response check",
			response: &http.Response{
				Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
				Body:   ioutil.NopCloser(strings.NewReader("{}")),
			},
			wantErr: false,
		},
		{
			name: "Valid answer without header and response check",
			response: &http.Response{
				StatusCode: http.StatusOK,
				Body:       ioutil.NopCloser(strings.NewReader("{}")),
			},
			wantErr: true,
		},
		{
			name:       "Request failed",
			response:   nil,
			requestErr: errors.New("request failed"),
			wantErr:    true,
		},
		{
			name: "Valid request, 500 response",
			response: &http.Response{
				StatusCode: http.StatusInternalServerError,
				Body:       ioutil.NopCloser(strings.NewReader("")),
			},
			requestErr: nil,
			wantErr:    false,
		},
		{
			name: "Valid request, 404 response",
			response: &http.Response{
				StatusCode: http.StatusNotFound,
				Body:       ioutil.NopCloser(strings.NewReader("")),
			},
			requestErr: nil,
			wantErr:    false,
		},
		{
			name: "Valid request, 403 response",
			response: &http.Response{
				StatusCode: http.StatusForbidden,
				Body:       ioutil.NopCloser(strings.NewReader("")),
			},
			requestErr: nil,
			wantErr:    false,
		},
		{
			name: "Valid request, 401 response",
			response: &http.Response{
				StatusCode: http.StatusUnauthorized,
				Body:       ioutil.NopCloser(strings.NewReader("")),
			},
			requestErr: nil,
			wantErr:    false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, _ := NewClient(Config{
				Transport: &mockTransp{RoundTripFunc: func(request *http.Request) (*http.Response, error) {
					return tt.response, tt.requestErr
				}},
			})
			_, err := c.Cat.Indices()
			if (err != nil) != tt.wantErr {
				t.Errorf("Unexpected error, got %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestProductCheckError(t *testing.T) {
	var requestPaths []string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestPaths = append(requestPaths, r.URL.Path)
		if len(requestPaths) == 1 {
			// Simulate transient error from a proxy on the first request.
			// This must not be cached by the client.
			w.WriteHeader(http.StatusBadGateway)
			return
		}

		w.Header().Set("X-Elastic-Product", "Elasticsearch")
		w.Write([]byte("{}"))
	}))
	defer server.Close()

	c, _ := NewClient(Config{Addresses: []string{server.URL}, DisableRetry: true})
	if _, err := c.Cat.Indices(); err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	if c.productCheckSuccess {
		t.Fatalf("product check should be invalid, got %v", c.productCheckSuccess)
	}
	if _, err := c.Cat.Indices(); err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	if n := len(requestPaths); n != 2 {
		t.Fatalf("expected 2 requests, got %d", n)
	}
	if !reflect.DeepEqual(requestPaths, []string{"/_cat/indices", "/_cat/indices"}) {
		t.Fatalf("unexpected request paths: %s", requestPaths)
	}
	if !c.productCheckSuccess {
		t.Fatalf("product check should be valid, got : %v", c.productCheckSuccess)
	}
}

func TestFingerprint(t *testing.T) {
	body := []byte(`{"body": true"}"`)
	cert, err := tls.X509KeyPair([]byte(`-----BEGIN CERTIFICATE-----
MIIDYjCCAkqgAwIBAgIVAIClHav09e9XGWJrnshywAjUHTnXMA0GCSqGSIb3DQEB
CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu
ZXJhdGVkIENBMB4XDTIzMDMyODE3MDIzOVoXDTI2MDMyNzE3MDIzOVowEzERMA8G
A1UEAxMIaW5zdGFuY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV
+t5/g6u2r3awCtzqp17KG0hRxzkVoJoF8DYzVh+Rv9ymxQW0C/U8dQihAjkZHaIA
n49lSyNLkwWtmqQgPcimV4d6XuTYx2ahDixXYtjmoOSwH5dRtovKPCNKDPkUj9Vq
NwMW0uB1VxniMKI4DnYFqBgHL9kQKhQqvas6Gx0X6ptGRCLYCtVxeFcau6nnkZJt
urb+HNV5waOh0uTmsqnnslK3NjCQ/f030vPKxM5fOqOU5ajUHpZFJ6ZFmS32074H
l+mZoRT/GtbnVtIg+CJXsWThF3/L4iBImv+rkY9MKX5fyMLJgmIJG68S90IQGR8c
Z2lZYzC0J7zjMsYlODbDAgMBAAGjgYswgYgwHQYDVR0OBBYEFIDIcECn3AVHc3jk
MpQ4r7Kc3WCsMB8GA1UdIwQYMBaAFJYCWKn16g+acbing4Vl45QGUBs0MDsGA1Ud
EQQ0MDKCCWxvY2FsaG9zdIIIaW5zdGFuY2WHBH8AAAGHEAAAAAAAAAAAAAAAAAAA
AAGCA2VzMTAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBtX3RQ5ATpfORM
lrnhaUPGOWkjnb3p3BrdAWUaWoh136QhaXqxKiALQQhTtTerkXOcuquy9MmAyYvS
9fDdGvLCAO8pPCXjnzonCHerCLGdS7f/eqvSFWCdy7LPHzTAFYfVWVvbZed+83TL
bDY63AMwIexj34vJEStMapuFwWx05fstE8qZWIbYCL87sF5H/MRhzlz3ScAhQ1N7
tODH7zvLzSxFGGEzCIKZ0iPFKbd3Y0wE6SptDSKhOqlnC8kkNeI2GjWsqVfHKsoF
pDFmri7IfOucuvalXJ6xiHPr9RDbuxEXs0u8mteT5nFQo7EaEGdHpg1pNGbfBOzP
lmj/dRS9
-----END CERTIFICATE-----`), []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAlfref4Ortq92sArc6qdeyhtIUcc5FaCaBfA2M1Yfkb/cpsUF
tAv1PHUIoQI5GR2iAJ+PZUsjS5MFrZqkID3IpleHel7k2MdmoQ4sV2LY5qDksB+X
UbaLyjwjSgz5FI/VajcDFtLgdVcZ4jCiOA52BagYBy/ZECoUKr2rOhsdF+qbRkQi
2ArVcXhXGrup55GSbbq2/hzVecGjodLk5rKp57JStzYwkP39N9LzysTOXzqjlOWo
1B6WRSemRZkt9tO+B5fpmaEU/xrW51bSIPgiV7Fk4Rd/y+IgSJr/q5GPTCl+X8jC
yYJiCRuvEvdCEBkfHGdpWWMwtCe84zLGJTg2wwIDAQABAoIBAAEP7HYNNnDWdYMD
+WAtYM12X/W5s/wUP94juaBI4u4iZH2EZodlixEdZUCTXgq43WsDUhxX05s7cE+p
H5DuSCHtoo2WHvGKAposwRDm2f3YVWQ2Xyb2ahNt69LYHHWrO+XQ60YYTa3r8Gn3
7dFR3I016/jyn5DeEVaglvS1dfj2UG4ybR4KkMfcKd94X0rKvz3wzAhHIh+hwMtv
sVk7V4vSnKf2mJXwIVECTolnEJEkCjWjjymgUJYKT8yN7JnAsHRcvMa6kWwIGrLp
oQCEaJwYM6ynCRS989pLt3vA2iu5VkYhiHXJ9Ds/5b5yzhzmj+ymzKbFKrrUUrmn
+2Jp1K0CgYEAw8BchALsD/+JuoXjinA14MH7PZjIsXyhtPk+c4pk42iMNyg1J8XF
Y/ITepLYsl2bZqQI1jOJdDqsTwIsva9r749lsmkYI3VOxhi7+qBK0sThR66C87lX
iU2QpnZ9NloC6ort4a3MEvZ/gRQcXdBrNlNoza2p7PHAVDTnsdSrNKUCgYEAxCQV
uo85oZyfnMufn/gcI9IeYOgiB0tO3a8cAFX2wQW1y935t6Z13ApUQc4EnCOH7ZBc
td5kT+xGdRWnfPZ38FM1dd5MBdGE69s3q8pJDUExSgNLqaF6/5bD32qui66L3ugu
eMjxrzqJsc2uQTPCs18SGsyRmf54DpY8HglOmUcCgYAGRDgx+a347SNJl1OrcOAo
q80RMbzrAaRjmL8JD9se9I/YjC73cPtasbsx51WMkDaTWJj30nqJ//7YIKeyAtWf
u6Vzyq19JRo6eTw7T7pVePwFQW7rwnks6hDBY3WqscL6IyxuVxP7X2zBgxVNY4ir
Gox2WSLhdPPFPlRUewxoCQKBgAJvqE1u5fpZ5ame5dao0ECppXLyrymEB/C88g4X
Az+WgJGNqkJbsO8QuccvdeMylcefmWcw4fIULzPZFwF4VjkH74wNPMh9t7buPBzI
IGwnuSMAM3ph5RMzni8yNgTKIDaej6U0abwRcBBjS5zHtc1giusGS3CsNnWH7Cs7
VlyVAoGBAK+prq9t9x3tC3NfCZH8/Wfs/X0T1qm11RiL5+tOhmbguWAqSSBy8OjX
Yh8AOXrFuMGldcaTXxMeiKvI2cyybnls1MFsPoeV/fSMJbex7whdeJeTi66NOSKr
oftUHvkHS0Vv/LicMEOufFGslb4T9aPJ7oyhoSlz9CfAutDWk/q/
-----END RSA PRIVATE KEY-----`))
	if err != nil {
		t.Fatal(err)
	}
	server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Elastic-Product", "Elasticsearch")
		w.Write(body)
	}))
	server.TLS = new(tls.Config)
	server.TLS.Certificates = []tls.Certificate{cert}
	server.StartTLS()

	defer server.Close()

	config := Config{
		Addresses:    []string{server.URL},
		DisableRetry: true,
	}

	// Without certificate and authority, client should fail on TLS
	client, _ := NewClient(config)
	_, err = client.Info()

	if errors.Unwrap(err).Error() != `x509: “instance” certificate is not standards compliant` {
		if ok := errors.As(err, &x509.UnknownAuthorityError{}); !ok {
			t.Fatalf("Uknown error, expected UnknownAuthorityError, got: %s", err)
		}
	}

	// We add the fingerprint corresponding to testcert.LocalhostCert
	//
	config.CertificateFingerprint = "1DBF91CA60E9B94E89582396C2C825466F4C449FAFB7BCA29157EE5D61D5C171"
	client, _ = NewClient(config)
	res, err := client.Info()
	if err != nil {
		t.Fatal(err)
	}
	defer res.Body.Close()

	data, _ := ioutil.ReadAll(res.Body)
	if !bytes.Equal(data, body) {
		t.Fatalf("unexpected payload returned: expected: %s, got: %s", body, data)
	}
}

func TestCompatibilityHeader(t *testing.T) {
	tests := []struct {
		name                string
		envVar              bool
		configVar           bool
		compatibilityHeader bool
		bodyPresent         bool
		expectsHeader       []string
	}{
		{
			name:                "Compatibility header disabled",
			compatibilityHeader: false,
			bodyPresent:         false,
			envVar:              false,
			configVar:           false,
			expectsHeader:       []string{"application/json"},
		},
		{
			name:                "Compatibility header enabled with env var",
			compatibilityHeader: true,
			bodyPresent:         false,
			envVar:              true,
			configVar:           false,
			expectsHeader:       []string{"application/vnd.elasticsearch+json;compatible-with=8"},
		},
		{
			name:                "Compatibility header enabled with body with env var",
			compatibilityHeader: true,
			bodyPresent:         true,
			envVar:              true,
			configVar:           false,
			expectsHeader:       []string{"application/vnd.elasticsearch+json;compatible-with=8"},
		},
		{
			name:                "Compatibility header enabled in conf",
			compatibilityHeader: true,
			bodyPresent:         false,
			envVar:              false,
			configVar:           true,
			expectsHeader:       []string{"application/vnd.elasticsearch+json;compatible-with=8"},
		},
		{
			name:                "Compatibility header enabled with body in conf",
			compatibilityHeader: true,
			bodyPresent:         true,
			envVar:              false,
			configVar:           true,
			expectsHeader:       []string{"application/vnd.elasticsearch+json;compatible-with=8"},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Setenv(esCompatHeader, strconv.FormatBool(test.compatibilityHeader))

			c, _ := NewClient(Config{
				EnableCompatibilityMode: test.configVar,
				Addresses:               []string{},
				Transport: &mockTransp{
					RoundTripFunc: func(req *http.Request) (*http.Response, error) {
						if test.compatibilityHeader {
							if !reflect.DeepEqual(req.Header["Accept"], test.expectsHeader) {
								t.Errorf("Compatibility header enabled but header is, not in request headers, got: %s, want: %s", req.Header["Accept"], test.expectsHeader)
							}

							if test.bodyPresent {
								if !reflect.DeepEqual(req.Header["Content-Type"], test.expectsHeader) {
									t.Errorf("Compatibility header with Body enabled, not in request headers, got: %s, want: %s", req.Header["Content-Type"], test.expectsHeader)
								}
							} else {
								if reflect.DeepEqual(req.Header["Content-Type"], test.expectsHeader) {
									t.Errorf("Compatibility header if Content-Type shouldn't be set with an empty body")
								}
							}
						}

						return &http.Response{
							StatusCode: http.StatusOK,
							Status:     "MOCK",
							Body:       ioutil.NopCloser(strings.NewReader("{}")),
						}, nil
					},
				},
			})

			req := &http.Request{URL: &url.URL{}, Header: make(http.Header)}
			if test.bodyPresent {
				req.Body = ioutil.NopCloser(strings.NewReader("{}"))
			}

			_, _ = c.Perform(req)
		})
	}
}

func TestBuildStrippedVersion(t *testing.T) {
	type args struct {
		version string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "Standard Go version",
			args: args{version: "go1.16"},
			want: "1.16",
		},
		{
			name: "Rc Go version",
			args: args{
				version: "go1.16rc1",
			},
			want: "1.16p",
		},
		{
			name: "Beta Go version (go1.16beta1 example)",
			args: args{
				version: "devel +2ff33f5e44 Thu Dec 17 16:03:19 2020 +0000",
			},
			want: "0.0p",
		},
		{
			name: "Random mostly good Go version",
			args: args{
				version: "1.16",
			},
			want: "1.16",
		},
		{
			name: "Client package version",
			args: args{
				version: "8.0.0",
			},
			want: "8.0.0",
		},
		{
			name: "Client pre release version",
			args: args{
				version: "8.0.0-SNAPSHOT",
			},
			want: "8.0.0p",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := buildStrippedVersion(tt.args.version); got != tt.want {
				t.Errorf("buildStrippedVersion() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMetaHeader(t *testing.T) {
	t.Run("MetaHeader with elastictransport", func(t *testing.T) {
		tp, _ := elastictransport.New(elastictransport.Config{
			URLs: []*url.URL{{Scheme: "http", Host: "foo"}},
			Transport: &mockTransp{
				RoundTripFunc: func(request *http.Request) (*http.Response, error) {
					h := request.Header.Get(HeaderClientMeta)
					if !metaHeaderReValidation.MatchString(h) {
						t.Errorf("expected client metaheader to validate regexp, got: %s", h)
					}

					return &http.Response{
						Header:     http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
						StatusCode: http.StatusOK,
						Status:     "OK",
						Body:       ioutil.NopCloser(strings.NewReader("")),
					}, nil
				},
			},
		})

		c, _ := NewDefaultClient()
		c.Transport = tp

		_, _ = c.Info()
	})

	t.Run("Metaheader with typedclient", func(t *testing.T) {
		tp, _ := elastictransport.New(elastictransport.Config{
			URLs: []*url.URL{{Scheme: "http", Host: "foo"}},
			Transport: &mockTransp{
				RoundTripFunc: func(request *http.Request) (*http.Response, error) {
					h := request.Header.Get(HeaderClientMeta)
					if !metaHeaderReValidation.MatchString(h) {
						t.Errorf("expected client metaheader to validate regexp, got: %s", h)
					}
					if !strings.Contains(h, "hl=1") {
						t.Errorf("invalid metaheader, should contain hl=1, got: %s", h)
					}

					return &http.Response{
						Header:     http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
						StatusCode: http.StatusOK,
						Status:     "OK",
						Body:       ioutil.NopCloser(strings.NewReader("")),
					}, nil
				},
			},
		})

		c, _ := NewTypedClient(Config{})
		c.Transport = tp

		_, _ = c.Info().Do(nil)
	})
}

func TestNewTypedClient(t *testing.T) {
	tp, _ := elastictransport.New(elastictransport.Config{
		URLs: []*url.URL{{Scheme: "http", Host: "foo"}},
		Transport: &mockTransp{
			RoundTripFunc: func(request *http.Request) (*http.Response, error) {
				h := request.Header.Get(HeaderClientMeta)
				if !metaHeaderReValidation.MatchString(h) {
					t.Errorf("expected client metaheader to validate regexp, got: %s", h)
				}

				return &http.Response{
					Header:     http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
					StatusCode: http.StatusOK,
					Status:     "OK",
					Body: ioutil.NopCloser(strings.NewReader(`{
					  "version" : {
						"number" : "8.0.0-SNAPSHOT",
						"build_flavor" : "default"
					  },
					  "tagline" : "You Know, for Search"
					}`)),
				}, nil
			},
		},
	})

	c, _ := NewTypedClient(Config{})
	c.Transport = tp

	res, err := c.Info().Do(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	if res.Tagline != "You Know, for Search" {
		t.Fatal("unexpected tagline")
	}

	_, err = NewClient(Config{})
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
}

func TestContentTypeOverride(t *testing.T) {
	t.Run("default JSON Content-Type", func(t *testing.T) {
		contentType := "application/json"

		tp, _ := elastictransport.New(elastictransport.Config{
			URLs: []*url.URL{{Scheme: "http", Host: "foo"}},
			Transport: &mockTransp{
				RoundTripFunc: func(request *http.Request) (*http.Response, error) {
					h := request.Header.Get("Content-Type")
					if h != contentType {
						t.Fatalf("unexpected content-type, wanted %s, got: %s", contentType, h)
					}

					return &http.Response{
						Header:     http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
						StatusCode: http.StatusOK,
						Status:     "OK",
						Body:       ioutil.NopCloser(strings.NewReader("")),
					}, nil
				},
			},
		})

		c, _ := NewDefaultClient()
		c.Transport = tp

		_, _ = c.Search(c.Search.WithBody(strings.NewReader("")))
	})
	t.Run("overriden CBOR Content-Type functional options style", func(t *testing.T) {
		contentType := "application/cbor"

		tp, _ := elastictransport.New(elastictransport.Config{
			URLs: []*url.URL{{Scheme: "http", Host: "foo"}},
			Transport: &mockTransp{
				RoundTripFunc: func(request *http.Request) (*http.Response, error) {
					h := request.Header.Get("Content-Type")
					if h != contentType {
						t.Fatalf("unexpected content-type, wanted %s, got: %s", contentType, h)
					}

					return &http.Response{
						Header:     http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
						StatusCode: http.StatusOK,
						Status:     "OK",
						Body:       ioutil.NopCloser(strings.NewReader("")),
					}, nil
				},
			},
		})

		c, _ := NewDefaultClient()
		c.Transport = tp

		_, _ = c.Search(
			c.Search.WithHeader(map[string]string{
				"Content-Type": contentType,
			}),
			c.Search.WithBody(strings.NewReader("")),
		)
	})
	t.Run("overriden CBOR Content-Type direct call style", func(t *testing.T) {
		contentType := "application/cbor"

		tp, _ := elastictransport.New(elastictransport.Config{
			URLs: []*url.URL{{Scheme: "http", Host: "foo"}},
			Transport: &mockTransp{
				RoundTripFunc: func(request *http.Request) (*http.Response, error) {
					h := request.Header.Get("Content-Type")
					if h != contentType {
						t.Fatalf("unexpected content-type, wanted %s, got: %s", contentType, h)
					}

					return &http.Response{
						Header:     http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
						StatusCode: http.StatusOK,
						Status:     "OK",
						Body:       ioutil.NopCloser(strings.NewReader("")),
					}, nil
				},
			},
		})

		c, _ := NewDefaultClient()
		c.Transport = tp

		search := esapi.SearchRequest{}
		search.Body = strings.NewReader("")
		search.Header = make(map[string][]string)
		search.Header.Set("Content-Type", contentType)
		search.Do(context.Background(), tp)
	})
}

type FakeInstrumentation struct {
	Error error

	Name                string
	Closed              bool
	ClusterID           string
	NodeName            string
	PathParts           map[string]string
	PersistQuery        bool
	QueryEndpoint       string
	Query               string
	BeforeRequestFunc   func(r *http.Request, endpoint string) bool
	BeforeRequestResult bool
	AfterRequestFunc    func(r *http.Request, system, endpoint string) bool
	AfterRequestResult  bool
}

func NewFakeInstrumentation(recordQuery bool) *FakeInstrumentation {
	return &FakeInstrumentation{
		PersistQuery: recordQuery,
		PathParts:    make(map[string]string),
	}
}

func (c *FakeInstrumentation) Start(ctx context.Context, name string) context.Context {
	c.Name = name
	return ctx
}

func (c *FakeInstrumentation) Close(ctx context.Context) {
	c.Closed = true
}

func (c *FakeInstrumentation) RecordError(ctx context.Context, err error) {
	c.Error = err
}

func (c *FakeInstrumentation) AfterResponse(ctx context.Context, res *http.Response) {
	if id := res.Header.Get("X-Found-Handling-Cluster"); id != "" {
		c.ClusterID = id
	}
	if name := res.Header.Get("X-Found-Handling-Instance"); name != "" {
		c.NodeName = name
	}
}

func (c *FakeInstrumentation) RecordPathPart(ctx context.Context, pathPart, value string) {
	c.PathParts[pathPart] = value
}

func (c *FakeInstrumentation) RecordRequestBody(ctx context.Context, endpoint string, query io.Reader) io.ReadCloser {
	c.QueryEndpoint = endpoint
	if !c.PersistQuery {
		return nil
	}

	buf := bytes.Buffer{}
	buf.ReadFrom(query)
	c.Query = buf.String()
	return io.NopCloser(&buf)
}

func (c *FakeInstrumentation) BeforeRequest(req *http.Request, endpoint string) {
	if c.BeforeRequestFunc != nil {
		c.BeforeRequestResult = c.BeforeRequestFunc(req, endpoint)
	}
}

func (c *FakeInstrumentation) AfterRequest(req *http.Request, system, endpoint string) {
	if c.AfterRequestFunc != nil {
		c.AfterRequestResult = c.AfterRequestFunc(req, system, endpoint)
	}
}

func TestInstrumentation(t *testing.T) {
	successTp := func(request *http.Request) (*http.Response, error) {
		h := http.Header{}
		h.Add("X-Elastic-Product", "Elasticsearch")
		h.Add("X-Found-Handling-Cluster", "foo-bar-cluster-id")
		h.Add("X-Found-Handling-Instance", "0123456789")
		return &http.Response{
			StatusCode: http.StatusOK,
			Header:     h,
			Body:       io.NopCloser(strings.NewReader(`{}`)),
		}, nil
	}

	errorTp := func(request *http.Request) (*http.Response, error) {
		h := http.Header{}
		h.Add("X-Elastic-Product", "Elasticsearch")
		h.Add("X-Found-Handling-Cluster", "foo-bar-cluster-id")
		h.Add("X-Found-Handling-Instance", "0123456789")
		return &http.Response{
			StatusCode: http.StatusNotFound,
			Header:     h,
			Body: io.NopCloser(strings.NewReader(`{
  "error": {
    "root_cause": [
      {
        "type": "index_not_found_exception",
        "reason": "no such index [foo]",
        "resource.type": "index_or_alias",
        "resource.id": "foo",
        "index_uuid": "_na_",
        "index": "foo"
      }
    ],
    "type": "index_not_found_exception",
    "reason": "no such index [foo]",
    "resource.type": "index_or_alias",
    "resource.id": "foo",
    "index_uuid": "_na_",
    "index": "foo"
  },
  "status": 404
}`)),
		}, nil
	}

	reason := "index_not_found_exception"

	type args struct {
		roundTripFunc   func(request *http.Request) (*http.Response, error)
		instrumentation *FakeInstrumentation
	}
	tests := []struct {
		name string
		args args
		want *FakeInstrumentation
	}{
		{
			name: "with query",
			args: args{
				successTp,
				NewFakeInstrumentation(true),
			},
			want: &FakeInstrumentation{
				Name:                "search",
				Closed:              true,
				ClusterID:           "foo-bar-cluster-id",
				NodeName:            "0123456789",
				PathParts:           map[string]string{"index": "foo"},
				PersistQuery:        true,
				QueryEndpoint:       "search",
				Query:               "{\"query\":{\"match_all\":{}}}",
				BeforeRequestResult: true,
				AfterRequestResult:  true,
				BeforeRequestFunc: func(r *http.Request, endpoint string) bool {
					if r != nil {
						if r.URL.Path != "/foo/_search" {
							return false
						}
						if r.Method != http.MethodPost {
							return false
						}
					}

					return true
				},
				AfterRequestFunc: func(r *http.Request, system, endpoint string) bool {
					if r != nil {
						if r.URL.Path != "/foo/_search" {
							return false
						}
						if r.Method != http.MethodPost {
							return false
						}
					}
					return true
				},
			},
		},
		{
			name: "without query",
			args: args{
				successTp,
				NewFakeInstrumentation(false),
			},
			want: &FakeInstrumentation{
				Name:                "search",
				Closed:              true,
				ClusterID:           "foo-bar-cluster-id",
				NodeName:            "0123456789",
				PathParts:           map[string]string{"index": "foo"},
				PersistQuery:        true,
				QueryEndpoint:       "search",
				Query:               "",
				BeforeRequestResult: true,
				AfterRequestResult:  true,
				BeforeRequestFunc:   func(r *http.Request, endpoint string) bool { return true },
				AfterRequestFunc:    func(r *http.Request, system, endpoint string) bool { return true },
			},
		},
		{
			name: "with error",
			args: args{
				errorTp,
				NewFakeInstrumentation(false),
			},
			want: &FakeInstrumentation{
				Name:                "search",
				Closed:              true,
				ClusterID:           "foo-bar-cluster-id",
				NodeName:            "0123456789",
				PathParts:           map[string]string{"index": "foo"},
				PersistQuery:        true,
				QueryEndpoint:       "search",
				Query:               "",
				BeforeRequestResult: true,
				AfterRequestResult:  true,
				BeforeRequestFunc:   func(r *http.Request, endpoint string) bool { return true },
				AfterRequestFunc:    func(r *http.Request, system, endpoint string) bool { return true },
				Error:               &types.ElasticsearchError{Status: http.StatusNotFound, ErrorCause: types.ErrorCause{Type: reason}},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Run("typed client", func(t *testing.T) {
				instrument := test.args.instrumentation
				instrument.BeforeRequestFunc = test.want.BeforeRequestFunc
				instrument.AfterRequestFunc = test.want.AfterRequestFunc

				es, _ := NewTypedClient(Config{
					Transport:       &mockTransp{RoundTripFunc: test.args.roundTripFunc},
					Instrumentation: instrument,
				})
				es.Search().
					Index("foo").
					Query(&types.Query{
						MatchAll: types.NewMatchAllQuery(),
					}).
					Do(context.Background())

				if test.want.Error != nil {
					if !errors.Is(instrument.Error, test.want.Error) {
						t.Error("ploup")
					}
				}

				if instrument.BeforeRequestResult != test.want.BeforeRequestResult ||
					instrument.AfterRequestResult != test.want.AfterRequestResult ||
					instrument.Name != test.want.Name ||
					instrument.Query != test.want.Query ||
					instrument.QueryEndpoint != test.want.QueryEndpoint ||
					instrument.NodeName != test.want.NodeName ||
					instrument.ClusterID != test.want.ClusterID ||
					instrument.Closed == false {
					t.Errorf("instrument didn't record the expected values:\ngot:  %#v\nwant: %#v", instrument, test.want)
				}

				if !reflect.DeepEqual(instrument.PathParts, test.want.PathParts) {
					t.Errorf("path parts not within expected values, got: %#v, want: %#v", instrument.PathParts, test.want.PathParts)
				}
			})
			t.Run("low-level client", func(t *testing.T) {
				instrument := test.args.instrumentation
				instrument.BeforeRequestFunc = test.want.BeforeRequestFunc
				instrument.AfterRequestFunc = test.want.AfterRequestFunc

				es, _ := NewClient(Config{
					Transport:       &mockTransp{RoundTripFunc: test.args.roundTripFunc},
					Instrumentation: instrument,
				})
				es.Search(
					es.Search.WithIndex("foo"),
					es.Search.WithBody(strings.NewReader("{\"query\":{\"match_all\":{}}}")),
					es.Search.WithContext(context.Background()),
				)

				if test.want.Error != nil {
					if !errors.Is(instrument.Error, test.want.Error) {
						t.Error("ploup")
					}
				}

				if instrument.BeforeRequestResult != test.want.BeforeRequestResult ||
					instrument.AfterRequestResult != test.want.AfterRequestResult ||
					instrument.Name != test.want.Name ||
					instrument.Query != test.want.Query ||
					instrument.QueryEndpoint != test.want.QueryEndpoint ||
					instrument.NodeName != test.want.NodeName ||
					instrument.ClusterID != test.want.ClusterID ||
					instrument.Closed == false {
					t.Errorf("instrument didn't record the expected values:\ngot:  %#v\nwant: %#v", instrument, test.want)
				}

				if !reflect.DeepEqual(instrument.PathParts, test.want.PathParts) {
					t.Errorf("path parts not within expected values, got: %#v, want: %#v", instrument.PathParts, test.want.PathParts)
				}
			})
		})
	}

}