package gateway

import (
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
)

// TestProxy_StripsV1Prefix is the load-bearing assertion: the path
// the upstream sees is the inbound path minus "/v1".
func TestProxy_StripsV1Prefix(t *testing.T) {
	cases := []struct {
		name     string
		inbound  string
		upstream string
	}{
		{"storage get", "/v1/storage/get/datasets/x.parquet", "/storage/get/datasets/x.parquet"},
		{"catalog list", "/v1/catalog/list", "/catalog/list"},
		{"ingest", "/v1/ingest", "/ingest"},
		{"sql", "/v1/sql", "/sql"},
		{"deeply nested key", "/v1/storage/get/a/b/c/d.parquet", "/storage/get/a/b/c/d.parquet"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var got string
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				got = r.URL.Path
				w.WriteHeader(http.StatusOK)
				_, _ = w.Write([]byte("ok"))
			}))
			defer upstream.Close()

			u, _ := url.Parse(upstream.URL)
			h := NewProxyHandler(u)

			req := httptest.NewRequest(http.MethodGet, tc.inbound, nil)
			rr := httptest.NewRecorder()
			h.ServeHTTP(rr, req)

			if rr.Code != http.StatusOK {
				t.Errorf("status: got %d, want 200", rr.Code)
			}
			if got != tc.upstream {
				t.Errorf("upstream path: got %q, want %q", got, tc.upstream)
			}
		})
	}
}

// TestProxy_PreservesQueryString verifies ingestd's `?name=X` flows
// through unchanged. The proxy's Director only touches Path; RawQuery
// is set by httputil.NewSingleHostReverseProxy's standard director.
func TestProxy_PreservesQueryString(t *testing.T) {
	var gotQuery string
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotQuery = r.URL.RawQuery
		w.WriteHeader(http.StatusOK)
	}))
	defer upstream.Close()
	u, _ := url.Parse(upstream.URL)
	h := NewProxyHandler(u)

	req := httptest.NewRequest(http.MethodPost, "/v1/ingest?name=workers&debug=1", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if gotQuery != "name=workers&debug=1" {
		t.Errorf("query: got %q, want %q", gotQuery, "name=workers&debug=1")
	}
}

// TestProxy_PreservesBody verifies POST bodies flow through.
func TestProxy_PreservesBody(t *testing.T) {
	var gotBody string
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		b, _ := io.ReadAll(r.Body)
		gotBody = string(b)
		w.WriteHeader(http.StatusOK)
	}))
	defer upstream.Close()
	u, _ := url.Parse(upstream.URL)
	h := NewProxyHandler(u)

	req := httptest.NewRequest(http.MethodPost, "/v1/sql", strings.NewReader(`{"sql":"SELECT 1"}`))
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if gotBody != `{"sql":"SELECT 1"}` {
		t.Errorf("body: got %q, want %q", gotBody, `{"sql":"SELECT 1"}`)
	}
}

// TestProxy_UpstreamDown_502 verifies the default ErrorHandler maps
// connection failures to 502 Bad Gateway. We point at a freshly
// closed listener address to provoke a connect failure.
func TestProxy_UpstreamDown_502(t *testing.T) {
	// Bind + close to grab an unused port.
	srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	addr := srv.URL
	srv.Close() // now nothing listens on this port

	u, _ := url.Parse(addr)
	h := NewProxyHandler(u)
	req := httptest.NewRequest(http.MethodGet, "/v1/health", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusBadGateway {
		t.Errorf("status: got %d, want 502", rr.Code)
	}
}

// TestProxy_SubPathUpstream is the regression for the D6 scrum
// O-BLOCK finding: when the upstream URL has a non-empty path
// (e.g. running gateway behind a sub-path on a shared host),
// the prior order-of-operations let the default Director's
// singleJoiningSlash run BEFORE the /v1 strip, producing
// "/api/v1/storage/..." which then failed TrimPrefix("/v1") and
// forwarded the un-stripped path. Stripping FIRST means the join
// sees the clean path.
func TestProxy_SubPathUpstream(t *testing.T) {
	var got string
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got = r.URL.Path
		w.WriteHeader(http.StatusOK)
	}))
	defer upstream.Close()

	// Append a sub-path to the upstream URL. The proxy MUST forward
	// to /api/storage/get/x.parquet, NOT /api/v1/storage/get/x.parquet.
	u, _ := url.Parse(upstream.URL + "/api")
	h := NewProxyHandler(u)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/x.parquet", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	want := "/api/storage/get/x.parquet"
	if got != want {
		t.Errorf("upstream path under sub-path: got %q, want %q", got, want)
	}
}

// TestProxy_RewritesHost ensures the upstream Host header is the
// upstream's, not the inbound. Backing services route on chi which
// is host-agnostic, but if any future middleware checks Host (e.g.
// CORS allow-list) it should see the right value.
func TestProxy_RewritesHost(t *testing.T) {
	var gotHost string
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotHost = r.Host
		w.WriteHeader(http.StatusOK)
	}))
	defer upstream.Close()
	u, _ := url.Parse(upstream.URL)
	h := NewProxyHandler(u)

	req := httptest.NewRequest(http.MethodGet, "/v1/health", nil)
	req.Host = "gateway.example.com"
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if gotHost != u.Host {
		t.Errorf("Host: got %q, want %q", gotHost, u.Host)
	}
}
