package route

import (
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"

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

func TestRouter_Match(t *testing.T) {
	discovery := &MockDiscovery{
		Results: map[string]string{
			"tidb42|^db-[a-z0-9]+-pd$":    "db-42-pd",
			"tidb100|^db-[a-z0-9]+-tikv$": "db-100-tikv",
			"tidbc1|^db-[a-z0-9]+-pd$":    "db-c1-pd", // for invalid port test
		},
		Errs: map[string]error{},
	}

	tests := []struct {
		name           string
		config         *config.Config
		path           string
		wantBaseURL    string
		wantSuffixPath string
		wantName       string
		wantErr        bool
		wantBadRequest bool
		wantPathParams map[string]string
	}{
		// Cluster routes - basic
		{
			name: "cluster route keeps service prefix",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "pd-api",
						Match: config.MatchConfig{Prefix: "/pd"},
						Backend: config.BackendConfig{
							Scheme:             "http",
							Port:               2379,
							ServiceNamePattern: "^db-[a-z0-9]+-pd$",
						},
						TLS: config.TLSConfig{Enabled: false},
					},
				},
			},
			path:           "/proxy/cluster/42/pd/pd/api/v1/config",
			wantBaseURL:    "http://db-42-pd.tidb42.svc:2379",
			wantSuffixPath: "/pd/api/v1/config",
			wantName:       "pd-api",
		},
		{
			name: "cluster route tikv service",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "tikv-status",
						Match: config.MatchConfig{Prefix: "/tikv"},
						Backend: config.BackendConfig{
							Scheme:             "http",
							Port:               20160,
							ServiceNamePattern: "^db-[a-z0-9]+-tikv$",
						},
						TLS: config.TLSConfig{Enabled: false},
					},
				},
			},
			path:           "/proxy/cluster/100/tikv/status",
			wantBaseURL:    "http://db-100-tikv.tidb100.svc:20160",
			wantSuffixPath: "/status",
			wantName:       "tikv-status",
		},
		{
			name: "tibr route with path parameter",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "tibr-api",
						Match: config.MatchConfig{Prefix: "/tibr/{name}"},
						Backend: config.BackendConfig{
							Scheme:              "https",
							Port:                19500,
							ServiceNameTemplate: "{name}-tibr-headless",
						},
						TLS: config.TLSConfig{
							Enabled:            true,
							InsecureSkipVerify: true,
						},
					},
				},
			},
			path:           "/proxy/cluster/99/tibr/demo-tibr/api/v1/status",
			wantBaseURL:    "https://demo-tibr-tibr-headless.tidb99.svc:19500",
			wantSuffixPath: "/api/v1/status",
			wantName:       "tibr-api",
			wantPathParams: map[string]string{"name": "demo-tibr"},
		},
		{
			name: "tibr route without remaining path",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "tibr-api",
						Match: config.MatchConfig{Prefix: "/tibr/{name}"},
						Backend: config.BackendConfig{
							Scheme:              "https",
							Port:                19500,
							ServiceNameTemplate: "{name}-tibr-headless",
						},
						TLS: config.TLSConfig{
							Enabled:            true,
							InsecureSkipVerify: true,
						},
					},
				},
			},
			path:           "/proxy/cluster/1000/tibr/example",
			wantBaseURL:    "https://example-tibr-headless.tidb1000.svc:19500",
			wantSuffixPath: "",
			wantName:       "tibr-api",
			wantPathParams: map[string]string{"name": "example"},
		},
		{
			name: "dm route with path parameter",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "dm-api",
						Match: config.MatchConfig{Prefix: "/dm/{id}"},
						Backend: config.BackendConfig{
							Scheme:              "https",
							Port:                8261,
							ServiceNameTemplate: "dbcluster-{id}-dm-master",
						},
						TLS: config.TLSConfig{
							Enabled:            true,
							InsecureSkipVerify: true,
						},
					},
				},
			},
			path:           "/proxy/cluster/42/dm/2064540458686427136/api/v1/tasks",
			wantBaseURL:    "https://dbcluster-2064540458686427136-dm-master.tidb42.svc:8261",
			wantSuffixPath: "/api/v1/tasks",
			wantName:       "dm-api",
			wantPathParams: map[string]string{"id": "2064540458686427136"},
		},
		{
			name: "existing static cluster route still works",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "pd-api-static",
						Match: config.MatchConfig{Prefix: "/pd"},
						Backend: config.BackendConfig{
							Scheme:             "http",
							Port:               2379,
							ServiceNamePattern: "^db-[a-z0-9]+-pd$",
						},
					},
				},
			},
			path:           "/proxy/cluster/42/pd/status",
			wantBaseURL:    "http://db-42-pd.tidb42.svc:2379",
			wantSuffixPath: "/status",
			wantName:       "pd-api-static",
		},
		// Service routes - without config
		{
			name:           "service route basic",
			config:         &config.Config{},
			path:           "/proxy/services/my-svc/namespaces/default/ports/8080/api/v1/status",
			wantBaseURL:    "http://my-svc.default.svc:8080",
			wantSuffixPath: "/api/v1/status",
			wantName:       "service:my-svc:default:8080",
		},
		{
			name:           "service route different namespace and port",
			config:         &config.Config{},
			path:           "/proxy/services/backend-api/namespaces/prod/ports/9090/metrics",
			wantBaseURL:    "http://backend-api.prod.svc:9090",
			wantSuffixPath: "/metrics",
			wantName:       "service:backend-api:prod:9090",
		},
		{
			name:           "service route name with hyphens",
			config:         &config.Config{},
			path:           "/proxy/services/my-backend-api/namespaces/default/ports/8080/api",
			wantBaseURL:    "http://my-backend-api.default.svc:8080",
			wantSuffixPath: "/api",
			wantName:       "service:my-backend-api:default:8080",
		},
		// Service routes - with config overrides
		{
			name: "service route config overrides scheme",
			config: &config.Config{
				ServiceRoutes: []config.RouteConfig{
					{
						Name:    "my-svc-https",
						Match:   config.MatchConfig{Prefix: "/my-svc"},
						Backend: config.BackendConfig{Scheme: "https"},
						TLS:     config.TLSConfig{Enabled: false},
					},
				},
			},
			path:           "/proxy/services/my-svc/namespaces/default/ports/8080/api",
			wantBaseURL:    "https://my-svc.default.svc:8080",
			wantSuffixPath: "/api",
			wantName:       "my-svc-https",
		},
		{
			name: "service route config enables TLS",
			config: &config.Config{
				ServiceRoutes: []config.RouteConfig{
					{
						Name:    "secure-svc",
						Match:   config.MatchConfig{Prefix: "/secure-svc"},
						Backend: config.BackendConfig{},
						TLS:     config.TLSConfig{Enabled: true},
					},
				},
			},
			path:           "/proxy/services/secure-svc/namespaces/default/ports/443/api",
			wantBaseURL:    "https://secure-svc.default.svc:443",
			wantSuffixPath: "/api",
			wantName:       "secure-svc",
		},
		{
			name: "service route config overrides port",
			config: &config.Config{
				ServiceRoutes: []config.RouteConfig{
					{
						Name:    "my-svc-custom-port",
						Match:   config.MatchConfig{Prefix: "/my-svc"},
						Backend: config.BackendConfig{Port: 8443},
						TLS:     config.TLSConfig{Enabled: false},
					},
				},
			},
			path:           "/proxy/services/my-svc/namespaces/default/ports/8080/api",
			wantBaseURL:    "http://my-svc.default.svc:8443",
			wantSuffixPath: "/api",
			wantName:       "my-svc-custom-port",
		},

		// Error cases
		{
			name: "invalid port returns error",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "pd-api",
						Match: config.MatchConfig{Prefix: "/pd"},
						Backend: config.BackendConfig{
							Scheme:             "http",
							Port:               0,
							ServiceNamePattern: "^db-[a-z0-9]+-pd$",
						},
						TLS: config.TLSConfig{Enabled: false},
					},
				},
			},
			path:           "/proxy/cluster/c1/pd/api",
			wantErr:        true,
			wantBadRequest: true,
		},
		{
			name: "tibr route does not match different service",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "tibr-api",
						Match: config.MatchConfig{Prefix: "/tibr/{name}"},
						Backend: config.BackendConfig{
							Scheme:              "https",
							Port:                19500,
							ServiceNameTemplate: "{name}-tibr-headless",
						},
						TLS: config.TLSConfig{
							Enabled:            true,
							InsecureSkipVerify: true,
						},
					},
				},
			},
			path:    "/proxy/cluster/1/pd/status",
			wantErr: true,
		},
		{
			name: "static route does not match parameterized path",
			config: &config.Config{
				ClusterRoutes: []config.RouteConfig{
					{
						Name:  "pd-api",
						Match: config.MatchConfig{Prefix: "/pd"},
						Backend: config.BackendConfig{
							Scheme:             "http",
							Port:               2379,
							ServiceNamePattern: "^db-[a-z0-9]+-pd$",
						},
						TLS: config.TLSConfig{Enabled: false},
					},
				},
			},
			path:    "/proxy/cluster/1/tibr/demo/api",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r, err := NewRouterWithDiscovery(tt.config, discovery)
			if err != nil {
				t.Fatalf("router init error: %v", err)
			}

			m, err := r.Match(context.Background(), tt.path)

			if tt.wantErr {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				if tt.wantBadRequest && !IsBadRequest(err) {
					t.Fatalf("expected bad request error, got %v", err)
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			if m.BackendURL.String() != tt.wantBaseURL {
				t.Errorf("BackendURL mismatch\ngot:  %s\nwant: %s", m.BackendURL, tt.wantBaseURL)
			}
			if m.Vars.Remaining != tt.wantSuffixPath {
				t.Errorf("SuffixPath mismatch\ngot:  %s\nwant: %s", m.Vars.Remaining, tt.wantSuffixPath)
			}
			if m.Name != tt.wantName {
				t.Errorf("Name mismatch\ngot:  %s\nwant: %s", m.Name, tt.wantName)
			}
			if tt.wantPathParams != nil && !reflect.DeepEqual(m.Vars.PathParams, tt.wantPathParams) {
				t.Errorf("PathParams mismatch\ngot:  %#v\nwant: %#v", m.Vars.PathParams, tt.wantPathParams)
			}
		})
	}
}

func TestRouter_DiscoveryError(t *testing.T) {
	discovery := &MockDiscovery{
		Errs: map[string]error{
			"tidb1|^pd$": errors.New("boom"),
		},
	}
	cfg := &config.Config{
		ClusterRoutes: []config.RouteConfig{
			{
				Name:  "pd-api",
				Match: config.MatchConfig{Prefix: "/pd"},
				Backend: config.BackendConfig{
					Port:               2379,
					ServiceNamePattern: "^pd$",
				},
			},
		},
	}

	r, err := NewRouterWithDiscovery(cfg, discovery)
	if err != nil {
		t.Fatalf("router init error: %v", err)
	}

	if _, err := r.Match(context.Background(), "/proxy/cluster/1/pd/api"); err == nil {
		t.Fatal("expected discovery error, got nil")
	}
}

func TestBuildServiceURL(t *testing.T) {
	tests := []struct {
		name      string
		scheme    string
		service   string
		namespace string
		port      int
		want      string
		wantErr   bool
	}{
		{
			name:      "basic http",
			scheme:    "http",
			service:   "my-svc",
			namespace: "default",
			port:      8080,
			want:      "http://my-svc.default.svc:8080",
		},
		{
			name:      "https scheme",
			scheme:    "https",
			service:   "secure-svc",
			namespace: "prod",
			port:      443,
			want:      "https://secure-svc.prod.svc:443",
		},
		{
			name:      "default scheme fallback",
			scheme:    "",
			service:   "api",
			namespace: "ns",
			port:      80,
			want:      "http://api.ns.svc:80",
		},
		{
			name:    "empty service",
			service: "",
			wantErr: true,
		},
		{
			name:      "invalid port low",
			service:   "svc",
			namespace: "ns",
			port:      0,
			wantErr:   true,
		},
		{
			name:      "invalid port high",
			service:   "svc",
			namespace: "ns",
			port:      70000,
			wantErr:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := buildServiceURL(tt.scheme, tt.service, tt.namespace, tt.port)

			if tt.wantErr {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			if got.String() != tt.want {
				t.Fatalf("url mismatch: got %s, want %s", got.String(), tt.want)
			}
		})
	}
}

func TestValidateDNSLabel(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		wantErr bool
	}{
		{name: "valid simple name", input: "my-service", wantErr: false},
		{name: "valid with numbers", input: "service-123", wantErr: false},
		{name: "valid single char", input: "a", wantErr: false},
		{name: "valid 63 chars", input: strings.Repeat("a", 63), wantErr: false},
		{name: "valid start with number", input: "1-service", wantErr: false},
		{name: "valid end with number", input: "service-1", wantErr: false},
		{name: "empty string", input: "", wantErr: true},
		{name: "too long 64 chars", input: strings.Repeat("a", 64), wantErr: true},
		{name: "uppercase rejected", input: "MyService", wantErr: true},
		{name: "starts with hyphen", input: "-service", wantErr: true},
		{name: "ends with hyphen", input: "service-", wantErr: true},
		{name: "contains underscore", input: "my_service", wantErr: true},
		{name: "contains slash", input: "my/service", wantErr: true},
		{name: "contains dot", input: "my.service", wantErr: true},
		{name: "contains space", input: "my service", wantErr: true},
		{name: "special chars", input: "service@123", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateDNSLabel(tt.input)
			if (err != nil) != tt.wantErr {
				t.Errorf("validateDNSLabel(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
			}
		})
	}
}
