package backend

import (
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"
)

func TestValidateTenantAPIBase(t *testing.T) {
	tests := []struct {
		name    string
		raw     string
		wantErr bool
	}{
		{name: "http scheme rejected", raw: "http://ok.example.com", wantErr: true},
		{name: "empty rejected", raw: "", wantErr: true},
		{name: "no scheme rejected", raw: "ok.example.com", wantErr: true},
		{name: "userinfo rejected", raw: "https://user@ok.example.com", wantErr: true},
		{name: "userinfo with password rejected", raw: "https://user:pass@ok.example.com", wantErr: true},
		{name: "fragment rejected", raw: "https://ok.example.com#frag", wantErr: true},
		{name: "missing host rejected", raw: "https://", wantErr: true},
		{name: "bare host accepted", raw: "https://ok.example.com", wantErr: false},
		{name: "host with port accepted", raw: "https://ok.example.com:8443", wantErr: false},
		{name: "host with path accepted", raw: "https://ok.example.com/v1/extract", wantErr: false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidateTenantAPIBase(tc.raw)
			if tc.wantErr && err == nil {
				t.Fatalf("ValidateTenantAPIBase(%q) = nil, want error", tc.raw)
			}
			if !tc.wantErr && err != nil {
				t.Fatalf("ValidateTenantAPIBase(%q) = %v, want nil", tc.raw, err)
			}
		})
	}
}

// stubExtractorLookupIP replaces the package DNS resolver for the duration of
// the test and registers cleanup to restore it.
func stubExtractorLookupIP(t *testing.T, fn func(ctx context.Context, network, host string) ([]net.IP, error)) {
	t.Helper()
	orig := extractorLookupIP
	extractorLookupIP = fn
	t.Cleanup(func() { extractorLookupIP = orig })
}

func mustParseIPs(t *testing.T, strs ...string) []net.IP {
	t.Helper()
	ips := make([]net.IP, 0, len(strs))
	for _, s := range strs {
		ip := net.ParseIP(s)
		if ip == nil {
			t.Fatalf("bad test fixture IP %q", s)
		}
		ips = append(ips, ip)
	}
	return ips
}

func TestExtractorResolvePublicIPs(t *testing.T) {
	tests := []struct {
		name    string
		ips     []string
		wantErr bool
	}{
		{name: "loopback rejected", ips: []string{"127.0.0.1"}, wantErr: true},
		{name: "ipv6 loopback rejected", ips: []string{"::1"}, wantErr: true},
		{name: "rfc1918 class a rejected", ips: []string{"10.0.0.5"}, wantErr: true},
		{name: "rfc1918 class b rejected", ips: []string{"172.16.0.1"}, wantErr: true},
		{name: "rfc1918 class c rejected", ips: []string{"192.168.1.1"}, wantErr: true},
		{name: "metadata link-local rejected", ips: []string{"169.254.169.254"}, wantErr: true},
		{name: "ipv6 link-local rejected", ips: []string{"fe80::1"}, wantErr: true},
		{name: "ipv6 ula rejected", ips: []string{"fd00::1"}, wantErr: true},
		{name: "unspecified rejected", ips: []string{"0.0.0.0"}, wantErr: true},
		{name: "this network rejected", ips: []string{"0.1.2.3"}, wantErr: true},
		{name: "multicast rejected", ips: []string{"224.0.0.1"}, wantErr: true},
		{name: "cgnat rejected", ips: []string{"100.64.0.1"}, wantErr: true},
		{name: "aliyun metadata cgnat rejected", ips: []string{"100.100.100.200"}, wantErr: true},
		{name: "ietf protocol assignments rejected", ips: []string{"192.0.0.1"}, wantErr: true},
		{name: "test-net-1 documentation rejected", ips: []string{"192.0.2.1"}, wantErr: true},
		{name: "test-net-2 documentation rejected", ips: []string{"198.51.100.1"}, wantErr: true},
		{name: "test-net-3 documentation rejected", ips: []string{"203.0.113.1"}, wantErr: true},
		{name: "benchmarking rejected", ips: []string{"198.18.0.1"}, wantErr: true},
		{name: "6to4 relay anycast rejected", ips: []string{"192.88.99.1"}, wantErr: true},
		{name: "reserved class e rejected", ips: []string{"240.0.0.1"}, wantErr: true},
		{name: "limited broadcast rejected", ips: []string{"255.255.255.255"}, wantErr: true},
		{name: "ipv6 unspecified rejected", ips: []string{"::"}, wantErr: true},
		{name: "ipv6 discard-only rejected", ips: []string{"100::1"}, wantErr: true},
		{name: "ipv6 documentation rejected", ips: []string{"2001:db8::1"}, wantErr: true},
		{name: "ipv6 orchidv2 rejected", ips: []string{"2001:20::1"}, wantErr: true},
		{name: "ipv6 benchmarking rejected", ips: []string{"2001:2::1"}, wantErr: true},
		{name: "ipv6 deprecated orchid rejected", ips: []string{"2001:10::1"}, wantErr: true},
		{name: "ipv6 6to4 rejected", ips: []string{"2002::1"}, wantErr: true},
		{name: "ipv6 6to4 embedded loopback rejected", ips: []string{"2002:7f00:1::1"}, wantErr: true},
		{name: "ipv6 srv6 sid rejected", ips: []string{"5f00::1"}, wantErr: true},
		{name: "ipv6 rfc9637 documentation rejected", ips: []string{"3fff::1"}, wantErr: true},
		{name: "ipv6 dummy prefix rejected", ips: []string{"100:0:0:1::1"}, wantErr: true},
		{name: "ipv6 teredo rejected", ips: []string{"2001::1"}, wantErr: true},
		{name: "ipv6 multicast rejected", ips: []string{"ff02::1"}, wantErr: true},
		{name: "ipv4-mapped private rejected", ips: []string{"::ffff:10.0.0.5"}, wantErr: true},
		{name: "nat64 private rejected", ips: []string{"64:ff9b::a00:5"}, wantErr: true},
		{name: "nat64 loopback rejected", ips: []string{"64:ff9b::7f00:1"}, wantErr: true},
		{name: "nat64 metadata rejected", ips: []string{"64:ff9b::a9fe:a9fe"}, wantErr: true},
		{name: "nat64 cgnat metadata rejected", ips: []string{"64:ff9b::6464:64c8"}, wantErr: true},
		// RFC 8215 local-use NAT64 prefix: rejected outright (never
		// unwrapped — the embedded layout is network-defined).
		{name: "nat64 local-use private rejected", ips: []string{"64:ff9b:1::a00:5"}, wantErr: true},
		{name: "nat64 local-use loopback rejected", ips: []string{"64:ff9b:1::7f00:1"}, wantErr: true},
		{name: "nat64 local-use metadata rejected", ips: []string{"64:ff9b:1::a9fe:a9fe"}, wantErr: true},
		{name: "nat64 local-use embedded public rejected", ips: []string{"64:ff9b:1::808:808"}, wantErr: true},
		{name: "nat64 local-use last address rejected", ips: []string{"64:ff9b:1:ffff:ffff:ffff:ffff:ffff"}, wantErr: true},
		{name: "mixed public and cgnat rejected", ips: []string{"93.184.216.34", "100.100.100.200"}, wantErr: true},
		{name: "mixed public and private rejected", ips: []string{"93.184.216.34", "10.0.0.1"}, wantErr: true},
		{name: "mixed public and link-local rejected", ips: []string{"169.254.169.254", "93.184.216.34"}, wantErr: true},
		{name: "public only accepted", ips: []string{"93.184.216.34"}, wantErr: false},
		{name: "below cgnat range accepted", ips: []string{"100.63.255.255"}, wantErr: false},
		{name: "above cgnat range accepted", ips: []string{"100.128.0.1"}, wantErr: false},
		// Boundary-adjacent addresses just outside newly denied ranges stay
		// dialable.
		{name: "below 6to4 relay anycast accepted", ips: []string{"192.88.98.255"}, wantErr: false},
		{name: "above 6to4 relay anycast accepted", ips: []string{"192.88.100.1"}, wantErr: false},
		// IPv6 classification is fail-closed: only the allocated
		// global-unicast space 2000::/3 is eligible, and all of 2001::/23
		// (IETF Protocol Assignments) is denied inside it. Addresses outside
		// 2000::/3 — including IANA non-global/unallocated space — are
		// rejected even when no denylist entry names them.
		{name: "ietf protocol assignments v6 rejected", ips: []string{"2001:5::1"}, wantErr: true},
		{name: "nat64 local-use boundary above rejected (outside 2000::/3)", ips: []string{"64:ff9b:2::1"}, wantErr: true},
		{name: "above ipv6 benchmarking rejected (inside 2001::/23)", ips: []string{"2001:2:1::1"}, wantErr: true},
		{name: "above rfc9637 documentation rejected (outside 2000::/3)", ips: []string{"4000::1"}, wantErr: true},
		{name: "below srv6 sid range rejected (outside 2000::/3)", ips: []string{"5eff::1"}, wantErr: true},
		{name: "above srv6 sid range rejected (outside 2000::/3)", ips: []string{"6000::1"}, wantErr: true},
		// Boundary-adjacent addresses inside 2000::/3 but outside every
		// denied special-purpose range stay dialable.
		{name: "below 6to4 range accepted", ips: []string{"2001:ffff::1"}, wantErr: false},
		{name: "above 6to4 range accepted", ips: []string{"2003::1"}, wantErr: false},
		{name: "below rfc9637 documentation accepted", ips: []string{"3ffe::1"}, wantErr: false},
		{name: "public v6 accepted", ips: []string{"2606:4700:4700::1111"}, wantErr: false},
		// NAT64/IPv4-mapped forms of public addresses stay dialable: the
		// embedded IPv4 is validated with the same rules and is public.
		{name: "ipv4-mapped public accepted", ips: []string{"::ffff:8.8.8.8"}, wantErr: false},
		{name: "nat64 public accepted", ips: []string{"64:ff9b::808:808"}, wantErr: false},
		{name: "multiple public accepted", ips: []string{"93.184.216.34", "1.1.1.1", "2606:2800:220:1:248:1893:25c8:1946"}, wantErr: false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
				return mustParseIPs(t, tc.ips...), nil
			})
			got, err := extractorResolvePublicIPs(context.Background(), "example.com")
			if tc.wantErr {
				if err == nil {
					t.Fatalf("extractorResolvePublicIPs(%v) = nil, want error", tc.ips)
				}
				return
			}
			if err != nil {
				t.Fatalf("extractorResolvePublicIPs(%v) = %v, want nil", tc.ips, err)
			}
			if len(got) != len(tc.ips) {
				t.Fatalf("extractorResolvePublicIPs(%v) returned %d IPs, want %d", tc.ips, len(got), len(tc.ips))
			}
		})
	}
}

func TestExtractorResolvePublicIPsLiteralIP(t *testing.T) {
	// Literal IP hosts must skip DNS entirely but still pass the public check.
	stubExtractorLookupIP(t, func(_ context.Context, _, host string) ([]net.IP, error) {
		t.Errorf("resolver must not be called for literal IP host %q", host)
		return nil, errors.New("resolver called")
	})

	tests := []struct {
		host    string
		wantErr bool
	}{
		{host: "192.168.1.1", wantErr: true},
		{host: "169.254.169.254", wantErr: true},
		{host: "127.0.0.1", wantErr: true},
		{host: "0.1.2.3", wantErr: true},
		{host: "192.0.2.1", wantErr: true},
		{host: "198.51.100.1", wantErr: true},
		{host: "203.0.113.1", wantErr: true},
		{host: "::1", wantErr: true},
		{host: "2001:db8::1", wantErr: true},
		{host: "100.100.100.200", wantErr: true},
		{host: "198.18.0.1", wantErr: true},
		{host: "240.0.0.1", wantErr: true},
		{host: "255.255.255.255", wantErr: true},
		{host: "::ffff:127.0.0.1", wantErr: true},
		{host: "64:ff9b::7f00:1", wantErr: true},
		{host: "64:ff9b::a9fe:a9fe", wantErr: true},
		{host: "64:ff9b:1::7f00:1", wantErr: true},
		{host: "2002:7f00:1::1", wantErr: true},
		{host: "192.88.99.1", wantErr: true},
		{host: "2001:5::1", wantErr: true},
		{host: "4000::1", wantErr: true},
		{host: "6000::1", wantErr: true},
		{host: "93.184.216.34", wantErr: false},
		{host: "100.63.255.255", wantErr: false},
		{host: "100.128.0.1", wantErr: false},
		{host: "2606:2800:220:1:248:1893:25c8:1946", wantErr: false},
		{host: "2606:4700:4700::1111", wantErr: false},
		{host: "64:ff9b::808:808", wantErr: false},
	}
	for _, tc := range tests {
		_, err := extractorResolvePublicIPs(context.Background(), tc.host)
		if tc.wantErr && err == nil {
			t.Errorf("extractorResolvePublicIPs(%q) = nil, want error", tc.host)
		}
		if !tc.wantErr && err != nil {
			t.Errorf("extractorResolvePublicIPs(%q) = %v, want nil", tc.host, err)
		}
	}
}

func TestExtractorDialContextRejectsNonPublic(t *testing.T) {
	stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
		return mustParseIPs(t, "93.184.216.34", "169.254.169.254"), nil
	})
	_, err := extractorDialContext(context.Background(), "tcp", "evil.example.com:443")
	if err == nil {
		t.Fatal("extractorDialContext = nil, want non-public IP rejection")
	}
	if !strings.Contains(err.Error(), "non-public IP") {
		t.Fatalf("extractorDialContext error = %v, want non-public IP mention", err)
	}
}

func TestDialExtractorResolvedIPsFallsBackToNextValidatedAddress(t *testing.T) {
	ips := mustParseIPs(t, "2606:4700:4700::1111", "1.1.1.1")
	var dialed []string
	clientConn, serverConn := net.Pipe()
	t.Cleanup(func() {
		_ = clientConn.Close()
		_ = serverConn.Close()
	})

	conn, err := dialExtractorResolvedIPs(
		context.Background(),
		"tcp",
		"provider.example.com:443",
		"443",
		ips,
		func(_ context.Context, _, addr string) (net.Conn, error) {
			dialed = append(dialed, addr)
			if len(dialed) == 1 {
				return nil, errors.New("IPv6 unavailable")
			}
			return clientConn, nil
		},
	)
	if err != nil {
		t.Fatalf("dialExtractorResolvedIPs: %v", err)
	}
	if conn != clientConn {
		t.Fatalf("connection = %v, want successful second connection", conn)
	}
	want := []string{"[2606:4700:4700::1111]:443", "1.1.1.1:443"}
	if len(dialed) != len(want) {
		t.Fatalf("dialed = %v, want %v", dialed, want)
	}
	for i := range want {
		if dialed[i] != want[i] {
			t.Fatalf("dialed[%d] = %q, want %q", i, dialed[i], want[i])
		}
	}
}

func TestDialExtractorResolvedIPsSharesRemainingDeadlineAcrossAddresses(t *testing.T) {
	ips := mustParseIPs(t, "2606:4700:4700::1111", "1.1.1.1")
	clientConn, serverConn := net.Pipe()
	t.Cleanup(func() {
		_ = clientConn.Close()
		_ = serverConn.Close()
	})

	ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
	t.Cleanup(cancel)
	parentDeadline, ok := ctx.Deadline()
	if !ok {
		t.Fatal("parent context has no deadline")
	}
	var firstDeadline time.Time
	var attempts int
	conn, err := dialExtractorResolvedIPs(
		ctx,
		"tcp",
		"provider.example.com:443",
		"443",
		ips,
		func(attemptCtx context.Context, _, _ string) (net.Conn, error) {
			attempts++
			deadline, ok := attemptCtx.Deadline()
			if !ok {
				t.Fatal("dial attempt context has no deadline")
			}
			if attempts == 1 {
				firstDeadline = deadline
				return nil, context.DeadlineExceeded
			}
			return clientConn, nil
		},
	)
	if err != nil {
		t.Fatalf("dialExtractorResolvedIPs: %v", err)
	}
	if conn != clientConn {
		t.Fatalf("connection = %v, want successful second connection", conn)
	}
	if attempts != 2 {
		t.Fatalf("attempts = %d, want 2", attempts)
	}
	if !firstDeadline.Before(parentDeadline.Add(-4 * time.Second)) {
		t.Fatalf("first attempt deadline = %v, parent deadline = %v; first address retained too much of the shared budget", firstDeadline, parentDeadline)
	}
}

// mustExtractorTransport unwraps the scheme-checking RoundTripper and returns
// the underlying *http.Transport.
func mustExtractorTransport(t *testing.T, client *http.Client) *http.Transport {
	t.Helper()
	rt, ok := client.Transport.(httpsOnlyRoundTripper)
	if !ok {
		t.Fatalf("transport type = %T, want httpsOnlyRoundTripper", client.Transport)
	}
	tr, ok := rt.next.(*http.Transport)
	if !ok {
		t.Fatalf("wrapped transport type = %T, want *http.Transport", rt.next)
	}
	return tr
}

func TestTenantExtractorClientNoEnvProxy(t *testing.T) {
	t.Setenv("HTTPS_PROXY", "http://proxy.internal:8080")
	t.Setenv("HTTP_PROXY", "http://proxy.internal:8080")
	t.Setenv("NO_PROXY", "")

	client := NewTenantExtractorHTTPClient(5 * time.Second)
	tr := mustExtractorTransport(t, client)
	if tr.Proxy != nil {
		t.Fatal("transport Proxy must be nil so env proxies are never used")
	}
}

func TestTenantExtractorClientRejectsPlainHTTP(t *testing.T) {
	resolverCalled := false
	stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
		resolverCalled = true
		return nil, errors.New("resolver must not be called for plain-http requests")
	})

	client := NewTenantExtractorHTTPClient(5 * time.Second)
	_, err := client.Get("http://example.com/")
	if err == nil {
		t.Fatal("client.Get(http://...) = nil, want scheme rejection")
	}
	if !strings.Contains(err.Error(), "non-https") {
		t.Fatalf("client.Get(http://...) error = %v, want scheme mention", err)
	}
	if resolverCalled {
		t.Fatal("resolver was called for a plain-http request; scheme must be rejected before any dial")
	}
}

func TestTenantExtractorClientNoRedirects(t *testing.T) {
	client := NewTenantExtractorHTTPClient(5 * time.Second)
	if client.CheckRedirect == nil {
		t.Fatal("CheckRedirect must be set")
	}
	err := client.CheckRedirect(&http.Request{}, nil)
	if !errors.Is(err, http.ErrUseLastResponse) {
		t.Fatalf("CheckRedirect = %v, want http.ErrUseLastResponse", err)
	}
}

func TestTenantExtractorClientTLSConfigDefault(t *testing.T) {
	client := NewTenantExtractorHTTPClient(5 * time.Second)
	tr := mustExtractorTransport(t, client)
	// TLSClientConfig stays nil so the transport derives SNI/ServerName from
	// the request URL hostname; the dialer only validates resolved IPs and
	// never rewrites the URL host.
	if tr.TLSClientConfig != nil {
		t.Fatalf("TLSClientConfig = %+v, want nil (default, SNI from URL host)", tr.TLSClientConfig)
	}
	if tr.DialContext == nil {
		t.Fatal("DialContext must be set to the validating dialer")
	}
}

func TestTenantExtractorClientTimeout(t *testing.T) {
	if got := NewTenantExtractorHTTPClient(0).Timeout; got != extractorDefaultTimeout {
		t.Fatalf("Timeout = %v, want default %v", got, extractorDefaultTimeout)
	}
	if got := NewTenantExtractorHTTPClient(7 * time.Second).Timeout; got != 7*time.Second {
		t.Fatalf("Timeout = %v, want 7s", got)
	}
}

func TestCustomerEndpointClientNoEnvProxy(t *testing.T) {
	t.Setenv("HTTPS_PROXY", "http://proxy.internal:8080")
	t.Setenv("HTTP_PROXY", "http://proxy.internal:8080")
	t.Setenv("NO_PROXY", "")

	client := NewCustomerEndpointHTTPClient(5 * time.Second)
	public, loopback := mustCustomerTransports(t, client)
	if public.Proxy != nil || loopback.Proxy != nil {
		t.Fatal("transport Proxy must be nil so env proxies are never used")
	}
}

func TestCustomerEndpointClientRejectsPlainHTTP(t *testing.T) {
	resolverCalled := false
	stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
		resolverCalled = true
		return nil, errors.New("resolver must not be called for plain-http requests")
	})

	client := NewCustomerEndpointHTTPClient(5 * time.Second)
	_, err := client.Get("http://example.com/")
	if err == nil {
		t.Fatal("client.Get(http://example.com/) = nil, want scheme rejection")
	}
	if !strings.Contains(err.Error(), "non-https") {
		t.Fatalf("client.Get(http://example.com/) error = %v, want scheme mention", err)
	}
	if resolverCalled {
		t.Fatal("resolver was called for a remote plain-http request")
	}
}

func TestCustomerEndpointClientAllowsLoopbackHTTP(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = io.WriteString(w, "ok")
	}))
	t.Cleanup(srv.Close)

	client := NewCustomerEndpointHTTPClient(5 * time.Second)
	resp, err := client.Get(srv.URL)
	if err != nil {
		t.Fatalf("loopback http GET: %v", err)
	}
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", resp.StatusCode)
	}
}

func TestCustomerEndpointClientRefusesRedirect(t *testing.T) {
	var hit atomic.Int32
	target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		hit.Add(1)
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(target.Close)
	redir := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, target.URL, http.StatusFound)
	}))
	t.Cleanup(redir.Close)

	client := NewCustomerEndpointHTTPClient(5 * time.Second)
	_, err := client.Get(redir.URL)
	if err == nil {
		t.Fatal("redirecting endpoint: want error")
	}
	if !errors.Is(err, errCustomerEndpointPolicy) {
		t.Fatalf("err = %v, want customer endpoint policy", err)
	}
	if hit.Load() != 0 {
		t.Fatal("followed HTTP redirect")
	}
}

func TestCustomerEndpointClientRejectsMetadataIP(t *testing.T) {
	stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
		return mustParseIPs(t, "169.254.169.254"), nil
	})
	client := NewCustomerEndpointHTTPClient(5 * time.Second)
	_, err := client.Get("https://sts.evil.example/")
	if err == nil {
		t.Fatal("metadata IP: want error")
	}
	if !errors.Is(err, errTenantExtractorEndpointPolicy) {
		t.Fatalf("err = %v, want extractor policy (non-public IP)", err)
	}
}

func TestCustomerEndpointClientRejectsAliyunMetadataIP(t *testing.T) {
	stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
		return mustParseIPs(t, "100.100.100.200"), nil
	})
	client := NewCustomerEndpointHTTPClient(5 * time.Second)
	_, err := client.Get("https://sts.evil.example/")
	if err == nil {
		t.Fatal("aliyun metadata IP: want error")
	}
	if !errors.Is(err, errTenantExtractorEndpointPolicy) {
		t.Fatalf("err = %v, want extractor policy (non-public IP)", err)
	}
}

func TestCustomerEndpointClientRejectsLocalhostRebind(t *testing.T) {
	stubExtractorLookupIP(t, func(_ context.Context, _, host string) ([]net.IP, error) {
		if host != "localhost" {
			t.Fatalf("unexpected host %q", host)
		}
		return mustParseIPs(t, "169.254.169.254"), nil
	})
	if _, err := resolveLoopbackIPs(context.Background(), "localhost"); err == nil {
		t.Fatal("localhost rebind to metadata: want error")
	}
}

func TestCustomerEndpointClientTimeout(t *testing.T) {
	if got := NewCustomerEndpointHTTPClient(0).Timeout; got != extractorDefaultTimeout {
		t.Fatalf("Timeout = %v, want default %v", got, extractorDefaultTimeout)
	}
	if got := NewCustomerEndpointHTTPClient(7 * time.Second).Timeout; got != 7*time.Second {
		t.Fatalf("Timeout = %v, want 7s", got)
	}
}

func mustCustomerTransports(t *testing.T, client *http.Client) (public, loopback *http.Transport) {
	t.Helper()
	rt, ok := client.Transport.(customerEndpointRoundTripper)
	if !ok {
		t.Fatalf("transport type = %T, want customerEndpointRoundTripper", client.Transport)
	}
	httpsRT, ok := rt.public.(httpsOnlyRoundTripper)
	if !ok {
		t.Fatalf("public transport type = %T, want httpsOnlyRoundTripper", rt.public)
	}
	public, ok = httpsRT.next.(*http.Transport)
	if !ok {
		t.Fatalf("public wrapped type = %T, want *http.Transport", httpsRT.next)
	}
	loopback, ok = rt.loopback.(*http.Transport)
	if !ok {
		t.Fatalf("loopback type = %T, want *http.Transport", rt.loopback)
	}
	return public, loopback
}
