package iceberg

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

	"github.com/gorilla/mux"
)

func TestIsValidTablePath(t *testing.T) {
	tests := []struct {
		tablePath string
		want      bool
	}{
		{"sales.orders", true},
		{"ns/table", true},
		{"ns//table", true},
		{"", true},
		{"../other", false},
		{"ns/../../etc", false},
		{"ns/./x", false},
		{`ns\..\x`, false},
	}
	for _, tt := range tests {
		if got := isValidTablePath(tt.tablePath); got != tt.want {
			t.Errorf("isValidTablePath(%q) = %v, want %v", tt.tablePath, got, tt.want)
		}
	}
}

func TestValidateRequestPath_RejectsTraversal(t *testing.T) {
	tests := []struct {
		name     string
		rawPath  string
		wantCode int
	}{
		{"clean namespace+table passes", "/v1/namespaces/sales/tables/orders", http.StatusOK},
		{"clean prefixed passes", "/v1/wh/namespaces/sales/tables/orders", http.StatusOK},
		{"clean namespace only passes", "/v1/namespaces/sales", http.StatusOK},

		// SkipClean(true) means raw `..` survives routing — these are the
		// realistic traversal shapes the middleware must catch.
		{"dotdot as prefix var rejected", "/v1/../namespaces/sales", http.StatusBadRequest},
		{"dotdot as namespace var rejected", "/v1/namespaces/..", http.StatusBadRequest},
		{"dotdot as namespace var prefixed rejected", "/v1/wh/namespaces/..", http.StatusBadRequest},
		{"dotdot as table var rejected", "/v1/namespaces/sales/tables/..", http.StatusBadRequest},
		{"dot as table var rejected", "/v1/namespaces/sales/tables/.", http.StatusBadRequest},
		// Iceberg clients send the 0x1F unit separator percent-encoded; mux
		// decodes it before the middleware sees the namespace var.
		{"unit-sep namespace with dotdot part rejected", "/v1/namespaces/sales%1F..%1Fevil", http.StatusBadRequest},
		{"leading unit-sep namespace rejected", "/v1/namespaces/%1Fsales", http.StatusBadRequest},
		{"trailing unit-sep namespace rejected", "/v1/namespaces/sales%1F", http.StatusBadRequest},
		{"consecutive unit-sep namespace rejected", "/v1/namespaces/sales%1F%1Fevil", http.StatusBadRequest},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			router := mux.NewRouter().SkipClean(true)
			router.Use(validateRequestPath)
			handlerCalled := false
			pass := func(w http.ResponseWriter, r *http.Request) {
				handlerCalled = true
				w.WriteHeader(http.StatusOK)
			}
			router.HandleFunc("/v1/namespaces/{namespace}", pass)
			router.HandleFunc("/v1/namespaces/{namespace}/tables/{table}", pass)
			router.HandleFunc("/v1/{prefix}/namespaces/{namespace}", pass)
			router.HandleFunc("/v1/{prefix}/namespaces/{namespace}/tables/{table}", pass)

			req := httptest.NewRequest(http.MethodGet, tt.rawPath, nil)
			rr := httptest.NewRecorder()
			router.ServeHTTP(rr, req)

			if rr.Code != tt.wantCode {
				t.Fatalf("path %q: got status %d, want %d (body=%q)", tt.rawPath, rr.Code, tt.wantCode, rr.Body.String())
			}
			if tt.wantCode == http.StatusBadRequest && handlerCalled {
				t.Fatalf("path %q: inner handler reached despite rejection", tt.rawPath)
			}
		})
	}
}

// Defense-in-depth: if a future route or middleware ever leaves one of the
// captured vars empty, the middleware must still reject the request. The
// default mux regex won't normally allow this.
func TestValidateRequestPath_RejectsEmptyCapturedVars(t *testing.T) {
	tests := []struct {
		name string
		vars map[string]string
	}{
		{"empty prefix", map[string]string{"prefix": "", "namespace": "ns"}},
		{"empty table", map[string]string{"namespace": "ns", "table": ""}},
		{"empty namespace", map[string]string{"namespace": ""}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handlerCalled := false
			h := validateRequestPath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				handlerCalled = true
			}))
			req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/", nil), tt.vars)
			rr := httptest.NewRecorder()
			h.ServeHTTP(rr, req)
			if handlerCalled {
				t.Fatalf("vars %v: inner handler reached despite empty capture", tt.vars)
			}
		})
	}
}

func TestIsValidNameSegment(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  bool
	}{
		{"empty ok", "", true},
		{"plain", "orders", true},
		{"with dot inside", "my.table", true},
		{"hidden", ".hidden", true},

		{"bare dot", ".", false},
		{"bare dotdot", "..", false},
		{"contains slash", "foo/bar", false},
		{"contains backslash", "foo\\bar", false},
		{"contains nul", "foo\x00bar", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := isValidNameSegment(tt.input); got != tt.want {
				t.Errorf("isValidNameSegment(%q) = %v, want %v", tt.input, got, tt.want)
			}
		})
	}
}
