package proxy

import (
	"bytes"
	"context"
	"crypto/tls"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"

	"github.com/tidbcloud/service-proxy/pkg/audit"
	"github.com/tidbcloud/service-proxy/pkg/config"
	"github.com/tidbcloud/service-proxy/pkg/route"
)

// testDiscovery returns a MockDiscovery that returns "test-service" for any query.
func testDiscovery() *route.MockDiscovery {
	return &route.MockDiscovery{
		Results: map[string]string{
			// Default pattern that matches any namespace with ".*-pd" pattern
			"tidbtest-cluster|.*-pd": "test-service",
		},
	}
}

func TestHandler_ServeHTTP(t *testing.T) {
	tests := []struct {
		name           string
		requestPath    string
		setupRouter    func() route.Router
		wantStatusCode int
	}{
		{
			name:        "invalid backend port returns 400",
			requestPath: "/proxy/cluster/test-cluster/pd/api/v1/members",
			setupRouter: func() route.Router {
				r, _ := route.NewRouterWithDiscovery(&config.Config{
					ClusterRoutes: []config.RouteConfig{
						{
							Name: "pd-api",
							Match: config.MatchConfig{
								Prefix: "/pd",
							},
							Backend: config.BackendConfig{
								Scheme:             "http",
								Port:               0,
								ServiceNamePattern: ".*-pd",
							},
							TLS: config.TLSConfig{Enabled: false},
						},
					},
				}, testDiscovery())
				return r
			},
			wantStatusCode: http.StatusBadRequest,
		},
		{
			name:        "route matched but backend unavailable returns 502",
			requestPath: "/proxy/cluster/test-cluster/pd/api/v1/members",
			setupRouter: func() route.Router {
				r, _ := route.NewRouterWithDiscovery(&config.Config{
					ClusterRoutes: []config.RouteConfig{
						{
							Name: "pd-api",
							Match: config.MatchConfig{
								Prefix: "/pd",
							},
							Backend: config.BackendConfig{
								Scheme:             "http",
								Port:               2379,
								ServiceNamePattern: ".*-pd",
							},
							TLS: config.TLSConfig{Enabled: false},
						},
					},
				}, testDiscovery())
				return r
			},
			wantStatusCode: http.StatusBadGateway,
		},
		{
			name:        "unknown service returns 404",
			requestPath: "/proxy/cluster/test-cluster/unknown/api",
			setupRouter: func() route.Router {
				r, _ := route.NewRouterWithDiscovery(&config.Config{
					ClusterRoutes: []config.RouteConfig{
						{
							Name: "pd-api",
							Match: config.MatchConfig{
								Prefix: "/pd",
							},
							Backend: config.BackendConfig{
								ServiceNamePattern: ".*-pd",
							},
						},
					},
				}, testDiscovery())
				return r
			},
			wantStatusCode: http.StatusNotFound,
		},
		{
			name:        "invalid path returns 404",
			requestPath: "/invalid/path",
			setupRouter: func() route.Router {
				r, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
				return r
			},
			wantStatusCode: http.StatusNotFound,
		},
		{
			name:        "completely invalid path returns 404",
			requestPath: "/foo/bar",
			setupRouter: func() route.Router {
				r, _ := route.NewRouterWithDiscovery(&config.Config{
					ClusterRoutes: []config.RouteConfig{
						{
							Name: "pd-api",
							Match: config.MatchConfig{
								Prefix: "/pd",
							},
							Backend: config.BackendConfig{
								ServiceNamePattern: ".*-pd",
							},
						},
					},
				}, testDiscovery())
				return r
			},
			wantStatusCode: http.StatusNotFound,
		},
		{
			name:        "missing cluster id returns 404",
			requestPath: "/proxy/cluster/",
			setupRouter: func() route.Router {
				r, _ := route.NewRouterWithDiscovery(&config.Config{
					ClusterRoutes: []config.RouteConfig{
						{
							Name: "pd-api",
							Match: config.MatchConfig{
								Prefix: "/pd",
							},
							Backend: config.BackendConfig{
								ServiceNamePattern: ".*-pd",
							},
						},
					},
				}, testDiscovery())
				return r
			},
			wantStatusCode: http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			router := tt.setupRouter()
			handler := NewHandler(router, nil, nil, nil)

			req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
			rec := httptest.NewRecorder()

			handler.ServeHTTP(rec, req)

			if rec.Code != tt.wantStatusCode {
				t.Errorf("expected status code %d, got %d", tt.wantStatusCode, rec.Code)
			}
		})
	}
}

func TestHandler_MultipleRequests(t *testing.T) {
	router, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(router, nil, nil, nil)

	tests := []struct {
		path           string
		wantStatusCode int
	}{
		{"/proxy/cluster/c1/pd/api", http.StatusNotFound},
		{"/proxy/cluster/c2/tikv/status", http.StatusNotFound},
		{"/proxy/services/svc/namespaces/ns/ports/8080/path", http.StatusBadGateway},
		{"/invalid/path", http.StatusNotFound},
	}

	for _, tt := range tests {
		req := httptest.NewRequest(http.MethodGet, tt.path, nil)
		rec := httptest.NewRecorder()

		handler.ServeHTTP(rec, req)

		if rec.Code != tt.wantStatusCode {
			t.Errorf("path %s: expected %d, got %d", tt.path, tt.wantStatusCode, rec.Code)
		}
	}
}

func TestHandler_buildReverseProxy(t *testing.T) {
	tests := []struct {
		name           string
		backendHandler http.HandlerFunc
		targetPath     string
		targetQuery    string
		requestPath    string // The path set before calling proxy (simulates ServeHTTP behavior)
		requestQuery   string
		wantStatus     int
		wantBody       string
	}{
		{
			name: "basic proxy forwards request and response",
			backendHandler: func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusOK)
				w.Write([]byte("hello from backend"))
			},
			targetPath:  "",
			requestPath: "/api/v1/status", // Already set to suffix path
			wantStatus:  http.StatusOK,
			wantBody:    "hello from backend",
		},
		{
			// Simulates: client requests /proxy/cluster/my-cluster/pd/api/v1/members
			// router resolves backend URL to http://pd-service:2379 (base URL only)
			// ServeHTTP sets req.URL.Path to /api/v1/members (suffix path)
			// Proxy should forward to /api/v1/members
			name: "proxy uses suffix path set by ServeHTTP",
			backendHandler: func(w http.ResponseWriter, r *http.Request) {
				w.Write([]byte("path:" + r.URL.Path))
			},
			targetPath:  "",
			requestPath: "/api/v1/members", // ServeHTTP sets this to suffix path
			wantStatus:  http.StatusOK,
			wantBody:    "path:/api/v1/members",
		},
		{
			name: "proxy handles backend 4xx response",
			backendHandler: func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusNotFound)
				w.Write([]byte("not found"))
			},
			targetPath:  "",
			requestPath: "/missing",
			wantStatus:  http.StatusNotFound,
			wantBody:    "not found",
		},
		{
			name: "proxy handles backend 5xx response",
			backendHandler: func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusInternalServerError)
				w.Write([]byte("server error"))
			},
			targetPath:  "",
			requestPath: "/error",
			wantStatus:  http.StatusInternalServerError,
			wantBody:    "server error",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock backend server
			backend := httptest.NewServer(tt.backendHandler)
			defer backend.Close()

			// Parse backend URL with target path
			targetURL, err := url.Parse(backend.URL + tt.targetPath)
			if err != nil {
				t.Fatalf("failed to parse target URL: %v", err)
			}
			if tt.targetQuery != "" {
				targetURL.RawQuery = tt.targetQuery
			}

			// Create handler
			r, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
			handler := NewHandler(r, nil, nil, nil)

			// Create matched route
			matched := &route.Route{
				Name:       "test-route",
				Config:     &config.RouteConfig{Name: "test-route"},
				BackendURL: targetURL,
				Vars:       &route.PathVars{},
			}

			// Build reverse proxy
			proxy := handler.buildReverseProxy(http.DefaultTransport, matched)

			// Create request
			reqURL := tt.requestPath
			if tt.requestQuery != "" {
				reqURL += "?" + tt.requestQuery
			}
			req := httptest.NewRequest(http.MethodGet, reqURL, nil)
			rec := httptest.NewRecorder()

			// Execute proxy
			proxy.ServeHTTP(rec, req)

			// Verify status code
			if rec.Code != tt.wantStatus {
				t.Errorf("status code: got %d, want %d", rec.Code, tt.wantStatus)
			}

			// Verify response body
			body, _ := io.ReadAll(rec.Body)
			if string(body) != tt.wantBody {
				t.Errorf("body: got %q, want %q", string(body), tt.wantBody)
			}
		})
	}
}

func TestHandler_buildReverseProxy_ErrorHandler(t *testing.T) {
	// Create handler
	r, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(r, nil, nil, nil)
	defer handler.Close()

	// Create target URL pointing to non-existent server
	targetURL, _ := url.Parse("http://localhost:1") // port 1 should fail to connect

	// Create matched route
	matched := &route.Route{
		Name:       "test-route",
		Config:     &config.RouteConfig{Name: "test-route"},
		BackendURL: targetURL,
		Vars:       &route.PathVars{},
	}

	// Build reverse proxy
	proxy := handler.buildReverseProxy(http.DefaultTransport, matched)

	// Create request
	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	rec := httptest.NewRecorder()

	// Execute proxy - should fail to connect
	proxy.ServeHTTP(rec, req)

	// Verify 502 Bad Gateway
	if rec.Code != http.StatusBadGateway {
		t.Errorf("status code: got %d, want %d", rec.Code, http.StatusBadGateway)
	}

	body, _ := io.ReadAll(rec.Body)
	if string(body) != "bad gateway\n" {
		t.Errorf("body: got %q, want %q", string(body), "bad gateway\n")
	}
}

func TestHandler_Close(t *testing.T) {
	router, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(router, nil, nil, nil)

	// Close should complete without error
	err := handler.Close()
	if err != nil {
		t.Errorf("Close() returned error: %v", err)
	}

	// Verify cache is empty after close
	cacheLen := 0
	handler.proxyCache.Range(func(_, _ interface{}) bool {
		cacheLen++
		return true
	})

	if cacheLen != 0 {
		t.Errorf("expected empty cache after Close, got %d entries", cacheLen)
	}
}

func TestHandler_ResponseWriter_StatusCapture(t *testing.T) {
	// Create handler
	r, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(r, nil, nil, nil)
	defer handler.Close()

	// Create target URL pointing to non-existent server
	targetURL, _ := url.Parse("http://localhost:1")

	// Create matched route
	matched := &route.Route{
		Name:       "test-route",
		Config:     &config.RouteConfig{Name: "test-route"},
		BackendURL: targetURL,
		Vars:       &route.PathVars{},
	}

	// Build reverse proxy
	proxy := handler.buildReverseProxy(http.DefaultTransport, matched)

	// Create request
	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	rec := httptest.NewRecorder()

	// Wrap with our responseWriter
	rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}

	// Execute proxy - should fail to connect and trigger ErrorHandler
	proxy.ServeHTTP(rw, req)

	// Verify responseWriter captured the 502 status
	if rw.statusCode != http.StatusBadGateway {
		t.Errorf("responseWriter status code: got %d, want %d", rw.statusCode, http.StatusBadGateway)
	}
}

func TestCreateTransport_InsecureSkipVerify(t *testing.T) {
	router, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(router, nil, nil, nil)
	defer handler.Close()

	tests := []struct {
		name                   string
		cfg                    *config.RouteConfig
		wantInsecureSkipVerify bool
		wantTLSConfig          bool
	}{
		{
			name: "InsecureSkipVerify enabled",
			cfg: &config.RouteConfig{
				TLS: config.TLSConfig{
					Enabled:            true,
					InsecureSkipVerify: true,
				},
			},
			wantInsecureSkipVerify: true,
			wantTLSConfig:          true,
		},
		{
			name: "TLS disabled",
			cfg: &config.RouteConfig{
				TLS: config.TLSConfig{
					Enabled: false,
				},
			},
			wantInsecureSkipVerify: false,
			wantTLSConfig:          false,
		},
		{
			name: "TLS enabled but no InsecureSkipVerify and no SecretRef",
			cfg: &config.RouteConfig{
				TLS: config.TLSConfig{
					Enabled:            true,
					InsecureSkipVerify: false,
					SecretRef:          config.SecretRefConfig{},
				},
			},
			wantInsecureSkipVerify: false,
			wantTLSConfig:          false,
		},
		{
			name: "InsecureSkipVerify takes precedence over SecretRef",
			cfg: &config.RouteConfig{
				TLS: config.TLSConfig{
					Enabled:            true,
					InsecureSkipVerify: true,
					SecretRef: config.SecretRefConfig{
						Name:      "some-secret",
						Namespace: "default",
					},
				},
			},
			wantInsecureSkipVerify: true,
			wantTLSConfig:          true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			transport, err := handler.createTransport(context.Background(), tt.cfg, "test-ns")
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			if tt.wantTLSConfig {
				if transport.TLSClientConfig == nil {
					t.Fatal("expected TLSClientConfig to be set")
				}
				if transport.TLSClientConfig.InsecureSkipVerify != tt.wantInsecureSkipVerify {
					t.Errorf("InsecureSkipVerify: got %v, want %v",
						transport.TLSClientConfig.InsecureSkipVerify, tt.wantInsecureSkipVerify)
				}
				if transport.TLSClientConfig.MinVersion != tls.VersionTLS12 {
					t.Errorf("MinVersion: got %d, want %d (TLS 1.2)",
						transport.TLSClientConfig.MinVersion, tls.VersionTLS12)
				}
			} else {
				if transport.TLSClientConfig != nil {
					t.Fatalf("expected TLSClientConfig to be nil, got %#v", transport.TLSClientConfig)
				}
			}
		})
	}
}

func TestHandler_TiBR_InsecureSkipVerify(t *testing.T) {
	// Create a self-signed HTTPS backend to simulate TiBR
	backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("tibr response"))
	}))
	defer backend.Close()

	// Parse backend URL
	backendURL, err := url.Parse(backend.URL)
	if err != nil {
		t.Fatalf("failed to parse backend URL %q: %v", backend.URL, err)
	}

	// Create handler
	router, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(router, nil, nil, nil)
	defer handler.Close()

	// Create TiBR-style route config with InsecureSkipVerify
	matched := &route.Route{
		Name: "tibr:test:tibr-default",
		Config: &config.RouteConfig{
			Name: "tibr-api",
			TLS: config.TLSConfig{
				Enabled:            true,
				InsecureSkipVerify: true,
			},
		},
		BackendURL: backendURL,
		Vars:       &route.PathVars{Namespace: "tidb123"},
	}

	// Get or create proxy (this should use InsecureSkipVerify)
	proxy, err := handler.getOrCreateProxy(context.Background(), matched)
	if err != nil {
		t.Fatalf("failed to get proxy: %v", err)
	}

	// Send request through proxy
	req := httptest.NewRequest(http.MethodGet, "/api/v1/backups", nil)
	rec := httptest.NewRecorder()
	proxy.ServeHTTP(rec, req)

	// Verify response - should succeed because InsecureSkipVerify bypasses cert validation
	if rec.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rec.Code)
	}

	body, err := io.ReadAll(rec.Body)
	if err != nil {
		t.Fatalf("failed to read response body: %v", err)
	}
	if string(body) != "tibr response" {
		t.Errorf("expected 'tibr response', got %q", string(body))
	}
}

func TestHandler_LargeBodyForwarding_WithAudit(t *testing.T) {
	// This test verifies that large request bodies are forwarded completely
	// even when audit is enabled with a smaller maxBodySize.
	// Previously, the code used LimitReader which would truncate the body.

	// Create a backend that echoes the request body
	var receivedBody []byte
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var err error
		receivedBody, err = io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("backend failed to read body: %v", err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("received"))
	}))
	defer backend.Close()

	// Parse backend URL
	backendURL, err := url.Parse(backend.URL)
	if err != nil {
		t.Fatalf("failed to parse backend URL: %v", err)
	}

	// Create temp audit file
	tmpDir := t.TempDir()
	auditFile := filepath.Join(tmpDir, "audit.log")

	// Create audit manager with small maxBodySize (256 bytes)
	auditManager, err := audit.NewManager(&audit.Config{
		Enabled:     true,
		FilePath:    auditFile,
		MaxBodySize: 256,
	})
	if err != nil {
		t.Fatalf("failed to create audit manager: %v", err)
	}
	defer auditManager.Close()

	// Create handler with audit enabled
	router, _ := route.NewRouterWithDiscovery(&config.Config{
		ServiceRoutes: []config.RouteConfig{
			{
				Name: "test-svc",
				Match: config.MatchConfig{
					Prefix: "/",
				},
				Backend: config.BackendConfig{
					Scheme: "http",
					Port:   8080,
				},
			},
		},
	}, testDiscovery())
	handler := NewHandler(router, nil, nil, auditManager)
	defer handler.Close()

	// Create matched route directly for this test
	matched := &route.Route{
		Name:       "test-route",
		Config:     &config.RouteConfig{Name: "test-route"},
		BackendURL: backendURL,
		Vars:       &route.PathVars{},
	}

	// Build reverse proxy
	proxy := handler.buildReverseProxy(http.DefaultTransport, matched)

	// Create a large request body (10KB - much larger than audit maxBodySize of 256)
	largeBody := strings.Repeat("X", 10*1024)
	req := httptest.NewRequest(http.MethodPost, "/upload", bytes.NewReader([]byte(largeBody)))
	rec := httptest.NewRecorder()

	// Wrap the body with audit wrapper (simulating what ServeHTTP does)
	maxBodySize := auditManager.GetMaxBodySize()
	auditBody := audit.NewBodyWrapper(req.Body, maxBodySize)
	req.Body = auditBody

	// Execute proxy
	proxy.ServeHTTP(rec, req)

	// Verify response
	if rec.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rec.Code)
	}

	// CRITICAL: Verify backend received the FULL body, not truncated
	if len(receivedBody) != len(largeBody) {
		t.Errorf("backend received %d bytes, expected %d bytes (body was truncated!)",
			len(receivedBody), len(largeBody))
	}

	// Verify audit captured only the first maxBodySize bytes
	auditData := auditBody.AuditBytes()
	if len(auditData) != maxBodySize {
		t.Errorf("audit captured %d bytes, expected %d bytes", len(auditData), maxBodySize)
	}
	if string(auditData) != strings.Repeat("X", maxBodySize) {
		t.Errorf("audit captured %q, expected first %d bytes of body", string(auditData), maxBodySize)
	}
	if !auditBody.Truncated() {
		t.Error("expected audit body to be truncated")
	}
}

func TestHandler_SmallBody_WithAudit(t *testing.T) {
	// Verify small bodies (under maxBodySize) are fully captured by audit

	var receivedBody []byte
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var err error
		receivedBody, err = io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("backend failed to read body: %v", err)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer backend.Close()

	backendURL, _ := url.Parse(backend.URL)

	tmpDir := t.TempDir()
	auditFile := filepath.Join(tmpDir, "audit.log")

	auditManager, err := audit.NewManager(&audit.Config{
		Enabled:     true,
		FilePath:    auditFile,
		MaxBodySize: 256,
	})
	if err != nil {
		t.Fatalf("failed to create audit manager: %v", err)
	}
	defer auditManager.Close()

	router, _ := route.NewRouterWithDiscovery(&config.Config{}, testDiscovery())
	handler := NewHandler(router, nil, nil, auditManager)
	defer handler.Close()

	matched := &route.Route{
		Name:       "test-route",
		Config:     &config.RouteConfig{Name: "test-route"},
		BackendURL: backendURL,
		Vars:       &route.PathVars{},
	}

	proxy := handler.buildReverseProxy(http.DefaultTransport, matched)

	// Small body (100 bytes, under the 256 limit)
	smallBody := strings.Repeat("Y", 100)
	req := httptest.NewRequest(http.MethodPost, "/small", bytes.NewReader([]byte(smallBody)))
	rec := httptest.NewRecorder()

	auditBody := audit.NewBodyWrapper(req.Body, auditManager.GetMaxBodySize())
	req.Body = auditBody

	proxy.ServeHTTP(rec, req)

	// Verify backend received full body
	if string(receivedBody) != smallBody {
		t.Errorf("backend received %q, expected %q", string(receivedBody), smallBody)
	}

	// Verify audit captured full body (since it's under the limit)
	auditData := auditBody.AuditBytes()
	if string(auditData) != smallBody {
		t.Errorf("audit captured %q, expected full body %q", string(auditData), smallBody)
	}
}
