package http_client

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/nexuscrm/outbound/url_safety"
)

// TestWebhookClient_RejectsInternalIP_PreflightSurfaces verifies that Do with
// a URL targeting a loopback/private address is rejected by the url_safety
// preflight before any network connection is made.
func TestWebhookClient_RejectsInternalIP_PreflightSurfaces(t *testing.T) {
	client := NewWebhookClient(url_safety.NewAllowlist())
	req, err := http.NewRequest("GET", "https://127.0.0.1/secret", nil)
	if err != nil {
		t.Fatalf("build request: %v", err)
	}
	_, err = client.Do(req)
	if err == nil {
		t.Fatal("expected error for loopback URL, got nil")
	}
	// Error must mention the preflight context.
	errStr := err.Error()
	if errStr == "" {
		t.Error("expected non-empty error string")
	}
}

// TestWebhookClient_HonorsHTTPProxyEnv verifies that the WebhookClient
// transport is configured with http.ProxyFromEnvironment (non-nil Proxy),
// which means HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars are honored at
// request time. We verify by reflect comparison to http.ProxyFromEnvironment
// — the transport field is a function, and the only safe way to confirm it
// is the env-based implementation without triggering Go's sync.Once proxy
// cache is to check the transport configuration directly.
func TestWebhookClient_HonorsHTTPProxyEnv(t *testing.T) {
	wc := NewWebhookClient(url_safety.NewAllowlist()).(*webhookClient)
	transport, ok := wc.client.Transport.(*http.Transport)
	if !ok {
		t.Fatalf("expected *http.Transport, got %T", wc.client.Transport)
	}
	if transport.Proxy == nil {
		t.Fatal("WebhookClient transport.Proxy must be non-nil (http.ProxyFromEnvironment) to honor HTTP_PROXY/HTTPS_PROXY/NO_PROXY")
	}
	// OutboundClient must remain direct-only (Proxy: nil) to verify split.
	outbound := NewOutboundClient(url_safety.NewAllowlist()).(*outboundClient)
	outboundTransport, ok2 := outbound.client.Transport.(*http.Transport)
	if !ok2 {
		t.Fatalf("outboundClient: expected *http.Transport, got %T", outbound.client.Transport)
	}
	if outboundTransport.Proxy != nil {
		t.Error("OutboundClient transport.Proxy must remain nil (direct-only); proxy support moved to WebhookClient")
	}
}

// TestWebhookClient_RedirectRevalidates verifies that when the server returns
// a 302 to an internal IP, the CheckRedirect hook rejects it via the allowlist.
// Uses a mixed allowlist that permits the initial loopback test server but
// blocks redirect to 10.0.0.1.
func TestWebhookClient_RedirectRevalidates(t *testing.T) {
	redirectSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "http://10.0.0.1/secret", http.StatusFound)
	}))
	defer redirectSrv.Close()

	// Build client directly with permissive allowlist so the initial request
	// passes preflight, then verify that checkRedirect blocks the redirect target.
	w := &webhookClient{
		allowlist: &mixedTestAllowlist{blockedPrefixes: []string{"10.0.0.1"}},
	}
	w.client = &http.Client{CheckRedirect: w.checkRedirect} // loopback-capable transport

	req, err := http.NewRequest("GET", redirectSrv.URL, nil)
	if err != nil {
		t.Fatalf("build request: %v", err)
	}
	_, err = w.Do(req)
	if err == nil {
		t.Fatal("expected error when redirect leads to 10.0.0.1, got nil")
	}
}

// TestWebhookClient_RefusesHTTPSDowngrade verifies that a same-host redirect
// from https → http is refused unconditionally. Uses checkRedirect directly
// to avoid the httptest port-mismatch problem (two test servers on 127.0.0.1
// have different ports → different Host strings, which would trigger the
// cross-host check rather than the scheme-downgrade check).
func TestWebhookClient_RefusesHTTPSDowngrade(t *testing.T) {
	wc := &webhookClient{allowlist: permissiveAllowlist{}}

	initialReq, _ := http.NewRequest("GET", "https://example.com/", nil)
	redirectReq, _ := http.NewRequest("GET", "http://example.com/", nil)

	err := wc.checkRedirect(redirectReq, []*http.Request{initialReq})
	if err == nil {
		t.Fatal("expected scheme downgrade refusal, got nil")
	}
	if !contains(err.Error(), "downgrade") {
		t.Errorf("expected 'downgrade' in error, got: %v", err)
	}
}

// TestWebhookClient_AllowsSameHostHTTPSRedirect verifies that a same-host
// HTTPS→HTTPS redirect (e.g. path normalization) is allowed.
func TestWebhookClient_AllowsSameHostHTTPSRedirect(t *testing.T) {
	wc := &webhookClient{allowlist: permissiveAllowlist{}}

	initialReq, _ := http.NewRequest("GET", "https://example.com/old", nil)
	redirectReq, _ := http.NewRequest("GET", "https://example.com/new", nil)

	err := wc.checkRedirect(redirectReq, []*http.Request{initialReq})
	if err != nil {
		t.Errorf("same-host HTTPS redirect must be allowed, got: %v", err)
	}
}

// TestWebhookClient_RefusesCrossHostRedirectUnconditionally verifies the
// round-12 architectural decision: cross-host redirects are refused regardless
// of what the initial request carries (mirrors the OutboundClient equivalent).
func TestWebhookClient_RefusesCrossHostRedirectUnconditionally(t *testing.T) {
	otherSrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
	}))
	defer otherSrv.Close()

	cases := []struct {
		name   string
		setup  func(req *http.Request, initialURL string) string
		header func(req *http.Request)
	}{
		{
			name:   "no credential signals",
			setup:  func(_ *http.Request, u string) string { return u },
			header: func(_ *http.Request) {},
		},
		{
			name:   "Authorization header",
			setup:  func(_ *http.Request, u string) string { return u },
			header: func(r *http.Request) { r.Header.Set("Authorization", "Bearer token") },
		},
		{
			name:   "X-Api-Key custom header",
			setup:  func(_ *http.Request, u string) string { return u },
			header: func(r *http.Request) { r.Header.Set("X-Api-Key", "secret") },
		},
		{
			name:   "URL query secret",
			setup:  func(_ *http.Request, u string) string { return u + "?api_key=secret" },
			header: func(_ *http.Request) {},
		},
		{
			name: "Slack-style path token",
			setup: func(_ *http.Request, u string) string {
				return u + "/services/T00000000/B00000000/SECRET"
			},
			header: func(_ *http.Request) {},
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run("cross-host refused: "+tc.name, func(t *testing.T) {
			initialSrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				http.Redirect(w, r, otherSrv.URL, http.StatusFound)
			}))
			defer initialSrv.Close()

			wc := &webhookClient{allowlist: permissiveAllowlist{}}
			innerClient := initialSrv.Client()
			innerClient.CheckRedirect = wc.checkRedirect
			wc.client = innerClient

			reqURL := tc.setup(nil, initialSrv.URL)
			req, err := http.NewRequest("GET", reqURL, nil)
			if err != nil {
				t.Fatalf("build request: %v", err)
			}
			tc.header(req)

			_, err = wc.Do(req)
			if err == nil {
				t.Fatal("expected cross-host refusal, got nil error")
			}
			if !contains(err.Error(), "cross-host") {
				t.Errorf("expected 'cross-host' in error, got: %v", err)
			}
		})
	}
}

// TestWebhookClient_Has30sTimeout verifies that WebhookClient has a 30s
// client-level Timeout (the deadline that was previously applied via
// context.WithTimeout in executeCallWebhook).
func TestWebhookClient_Has30sTimeout(t *testing.T) {
	wc := NewWebhookClient(url_safety.NewAllowlist()).(*webhookClient)
	if wc.client.Timeout != 30*time.Second {
		t.Errorf("expected client.Timeout == 30s, got %v", wc.client.Timeout)
	}
}

// contains is a simple substring check helper.
func contains(s, substr string) bool {
	if len(substr) == 0 {
		return true
	}
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
