package api

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

	"github.com/rcourtman/pulse-go-rewrite/internal/config"
)

func TestIsDirectLoopbackRequest(t *testing.T) {
	t.Helper()

	tests := []struct {
		name       string
		req        *http.Request
		remoteAddr string
		headers    map[string]string
		want       bool
	}{
		// Nil request
		{
			name: "nil request",
			req:  nil,
			want: false,
		},

		// Valid loopback IPs without proxy headers
		{
			name:       "loopback IPv4 without port",
			remoteAddr: "127.0.0.1",
			want:       true,
		},
		{
			name:       "loopback IPv4 with port",
			remoteAddr: "127.0.0.1:8080",
			want:       true,
		},
		{
			name:       "loopback IPv4 alternate",
			remoteAddr: "127.0.0.2:54321",
			want:       true,
		},
		{
			name:       "loopback IPv6 without port",
			remoteAddr: "::1",
			want:       true,
		},
		{
			name:       "loopback IPv6 with port",
			remoteAddr: "[::1]:8080",
			want:       true,
		},
		{
			name:       "loopback IPv6 with brackets no port",
			remoteAddr: "[::1]",
			want:       true,
		},

		// Loopback with proxy headers (should reject)
		{
			name:       "loopback with X-Forwarded-For",
			remoteAddr: "127.0.0.1:8080",
			headers: map[string]string{
				"X-Forwarded-For": "192.168.1.1",
			},
			want: false,
		},
		{
			name:       "loopback with Forwarded",
			remoteAddr: "127.0.0.1:8080",
			headers: map[string]string{
				"Forwarded": "for=192.168.1.1",
			},
			want: false,
		},
		{
			name:       "loopback with X-Real-IP",
			remoteAddr: "127.0.0.1:8080",
			headers: map[string]string{
				"X-Real-IP": "192.168.1.1",
			},
			want: false,
		},
		{
			name:       "loopback with multiple proxy headers",
			remoteAddr: "127.0.0.1:8080",
			headers: map[string]string{
				"X-Forwarded-For": "192.168.1.1",
				"X-Real-IP":       "10.0.0.1",
			},
			want: false,
		},
		{
			name:       "loopback IPv6 with X-Forwarded-For",
			remoteAddr: "[::1]:8080",
			headers: map[string]string{
				"X-Forwarded-For": "203.0.113.42",
			},
			want: false,
		},

		// Non-loopback IPs (should reject)
		{
			name:       "private IPv4",
			remoteAddr: "192.168.1.1:8080",
			want:       false,
		},
		{
			name:       "private IPv4 10.x",
			remoteAddr: "10.0.0.1:54321",
			want:       false,
		},
		{
			name:       "private IPv4 172.x",
			remoteAddr: "172.16.0.1:8080",
			want:       false,
		},
		{
			name:       "public IPv4",
			remoteAddr: "203.0.113.42:8080",
			want:       false,
		},
		{
			name:       "public IPv6",
			remoteAddr: "[2001:db8::1]:8080",
			want:       false,
		},
		{
			name:       "link-local IPv6",
			remoteAddr: "[fe80::1]:8080",
			want:       false,
		},

		// Edge cases
		{
			name:       "empty RemoteAddr",
			remoteAddr: "",
			want:       false,
		},
		{
			name:       "invalid IP format",
			remoteAddr: "not-an-ip:8080",
			want:       false,
		},
		{
			name:       "invalid IP with port",
			remoteAddr: "999.999.999.999:8080",
			want:       false,
		},
		{
			name:       "malformed IPv6",
			remoteAddr: "[::g]:8080",
			want:       false,
		},
		{
			name:       "just port",
			remoteAddr: ":8080",
			want:       false,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			var req *http.Request
			if tt.req != nil {
				req = tt.req
			} else if tt.name != "nil request" {
				req = httptest.NewRequest("GET", "/", nil)
				req.RemoteAddr = tt.remoteAddr
				for key, value := range tt.headers {
					req.Header.Set(key, value)
				}
			}

			got := isDirectLoopbackRequest(req)
			if got != tt.want {
				t.Errorf("isDirectLoopbackRequest() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestFirstForwardedValue(t *testing.T) {
	tests := []struct {
		name   string
		header string
		want   string
	}{
		// Empty/nil cases
		{
			name:   "empty string",
			header: "",
			want:   "",
		},

		// Single value
		{
			name:   "single IP",
			header: "192.168.1.1",
			want:   "192.168.1.1",
		},
		{
			name:   "single IP with whitespace",
			header: "  192.168.1.1  ",
			want:   "192.168.1.1",
		},
		{
			name:   "single IPv6",
			header: "2001:db8::1",
			want:   "2001:db8::1",
		},

		// Multiple values (comma-separated)
		{
			name:   "multiple IPs returns first",
			header: "192.168.1.1, 10.0.0.1, 172.16.0.1",
			want:   "192.168.1.1",
		},
		{
			name:   "multiple IPs with extra whitespace",
			header: "  203.0.113.42  ,  192.168.1.1  ",
			want:   "203.0.113.42",
		},
		{
			name:   "first value empty after split",
			header: ", 192.168.1.1",
			want:   "",
		},
		{
			name:   "only commas",
			header: ",,,",
			want:   "",
		},

		// Realistic proxy chain scenarios
		{
			name:   "proxy chain client first",
			header: "client.example.com, proxy1.example.com, proxy2.example.com",
			want:   "client.example.com",
		},
		{
			name:   "mixed IPv4 and IPv6 chain",
			header: "2001:db8::1, 192.168.1.1, 10.0.0.1",
			want:   "2001:db8::1",
		},

		// Edge cases
		{
			name:   "value with port (non-standard but seen in wild)",
			header: "192.168.1.1:8080, 10.0.0.1",
			want:   "192.168.1.1:8080",
		},
		{
			name:   "hostname instead of IP",
			header: "client.example.com",
			want:   "client.example.com",
		},
		{
			name:   "tabs and newlines stripped",
			header: "\t192.168.1.1\n",
			want:   "192.168.1.1",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := firstForwardedValue(tt.header)
			if got != tt.want {
				t.Errorf("firstForwardedValue(%q) = %q, want %q", tt.header, got, tt.want)
			}
		})
	}
}

func TestSanitizeForwardedHost(t *testing.T) {
	tests := []struct {
		name         string
		raw          string
		wantFull     string // host with port preserved
		wantHostOnly string // host without port, brackets stripped
	}{
		// Empty cases
		{
			name:         "empty string",
			raw:          "",
			wantFull:     "",
			wantHostOnly: "",
		},
		{
			name:         "only whitespace",
			raw:          "   ",
			wantFull:     "",
			wantHostOnly: "",
		},
		{
			name:         "only scheme http",
			raw:          "http://",
			wantFull:     "",
			wantHostOnly: "",
		},
		{
			name:         "only scheme https",
			raw:          "https://",
			wantFull:     "",
			wantHostOnly: "",
		},

		// Simple hostnames
		{
			name:         "simple hostname",
			raw:          "example.com",
			wantFull:     "example.com",
			wantHostOnly: "example.com",
		},
		{
			name:         "hostname with whitespace",
			raw:          "  example.com  ",
			wantFull:     "example.com",
			wantHostOnly: "example.com",
		},
		{
			name:         "fqdn",
			raw:          "api.example.com",
			wantFull:     "api.example.com",
			wantHostOnly: "api.example.com",
		},

		// Hostnames with ports
		{
			name:         "hostname with port",
			raw:          "example.com:8080",
			wantFull:     "example.com:8080",
			wantHostOnly: "example.com",
		},
		{
			name:         "hostname with standard https port",
			raw:          "example.com:443",
			wantFull:     "example.com:443",
			wantHostOnly: "example.com",
		},
		{
			name:         "hostname with standard http port",
			raw:          "example.com:80",
			wantFull:     "example.com:80",
			wantHostOnly: "example.com",
		},

		// With scheme prefixes
		{
			name:         "http scheme stripped",
			raw:          "http://example.com",
			wantFull:     "example.com",
			wantHostOnly: "example.com",
		},
		{
			name:         "https scheme stripped",
			raw:          "https://example.com",
			wantFull:     "example.com",
			wantHostOnly: "example.com",
		},
		{
			name:         "http scheme with port",
			raw:          "http://example.com:8080",
			wantFull:     "example.com:8080",
			wantHostOnly: "example.com",
		},
		{
			name:         "https scheme with port",
			raw:          "https://example.com:9443",
			wantFull:     "example.com:9443",
			wantHostOnly: "example.com",
		},

		// Trailing slashes/paths
		{
			name:         "trailing slash stripped",
			raw:          "example.com/",
			wantFull:     "example.com",
			wantHostOnly: "example.com",
		},
		{
			name:         "scheme and trailing slash",
			raw:          "https://example.com/",
			wantFull:     "example.com",
			wantHostOnly: "example.com",
		},

		// IPv4 addresses
		{
			name:         "IPv4 address",
			raw:          "192.168.1.1",
			wantFull:     "192.168.1.1",
			wantHostOnly: "192.168.1.1",
		},
		{
			name:         "IPv4 with port",
			raw:          "192.168.1.1:8080",
			wantFull:     "192.168.1.1:8080",
			wantHostOnly: "192.168.1.1",
		},
		{
			name:         "IPv4 with scheme",
			raw:          "http://10.0.0.1",
			wantFull:     "10.0.0.1",
			wantHostOnly: "10.0.0.1",
		},

		// IPv6 addresses (key edge case - bracket handling)
		{
			name:         "IPv6 with brackets",
			raw:          "[::1]",
			wantFull:     "[::1]",
			wantHostOnly: "::1",
		},
		{
			name:         "IPv6 with brackets and port",
			raw:          "[::1]:8080",
			wantFull:     "[::1]:8080",
			wantHostOnly: "::1",
		},
		{
			name:         "IPv6 full address with brackets",
			raw:          "[2001:db8::1]",
			wantFull:     "[2001:db8::1]",
			wantHostOnly: "2001:db8::1",
		},
		{
			name:         "IPv6 full address with brackets and port",
			raw:          "[2001:db8::1]:443",
			wantFull:     "[2001:db8::1]:443",
			wantHostOnly: "2001:db8::1",
		},
		{
			name:         "IPv6 with scheme",
			raw:          "https://[::1]:9443",
			wantFull:     "[::1]:9443",
			wantHostOnly: "::1",
		},
		{
			name:         "IPv6 without brackets (raw - no port possible)",
			raw:          "::1",
			wantFull:     "::1",
			wantHostOnly: "::1",
		},

		// Realistic forwarded host headers
		{
			name:         "X-Forwarded-Host typical",
			raw:          "api.myservice.com",
			wantFull:     "api.myservice.com",
			wantHostOnly: "api.myservice.com",
		},
		{
			name:         "reverse proxy with non-standard port",
			raw:          "internal.corp.local:7655",
			wantFull:     "internal.corp.local:7655",
			wantHostOnly: "internal.corp.local",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotFull, gotHostOnly := sanitizeForwardedHost(tt.raw)
			if gotFull != tt.wantFull {
				t.Errorf("sanitizeForwardedHost(%q) full = %q, want %q", tt.raw, gotFull, tt.wantFull)
			}
			if gotHostOnly != tt.wantHostOnly {
				t.Errorf("sanitizeForwardedHost(%q) hostOnly = %q, want %q", tt.raw, gotHostOnly, tt.wantHostOnly)
			}
		})
	}
}

func TestIsLoopbackHost(t *testing.T) {
	tests := []struct {
		name string
		host string
		want bool
	}{
		// Empty/special cases (treated as loopback for safety)
		{
			name: "empty string",
			host: "",
			want: true,
		},

		// Localhost keyword
		{
			name: "localhost lowercase",
			host: "localhost",
			want: true,
		},
		{
			name: "localhost uppercase",
			host: "LOCALHOST",
			want: true,
		},
		{
			name: "localhost mixed case",
			host: "LocalHost",
			want: true,
		},

		// IPv4 loopback range
		{
			name: "127.0.0.1",
			host: "127.0.0.1",
			want: true,
		},
		{
			name: "127.0.0.2 (full loopback range)",
			host: "127.0.0.2",
			want: true,
		},
		{
			name: "127.255.255.255 (end of loopback range)",
			host: "127.255.255.255",
			want: true,
		},

		// IPv6 loopback
		{
			name: "::1",
			host: "::1",
			want: true,
		},

		// Unspecified addresses (treated as loopback)
		{
			name: "0.0.0.0 unspecified IPv4",
			host: "0.0.0.0",
			want: true,
		},
		{
			name: ":: unspecified IPv6",
			host: "::",
			want: true,
		},

		// Non-loopback private addresses
		{
			name: "private 192.168.x",
			host: "192.168.1.1",
			want: false,
		},
		{
			name: "private 10.x",
			host: "10.0.0.1",
			want: false,
		},
		{
			name: "private 172.16.x",
			host: "172.16.0.1",
			want: false,
		},

		// Non-loopback public addresses
		{
			name: "public IPv4",
			host: "203.0.113.42",
			want: false,
		},
		{
			name: "public IPv6",
			host: "2001:db8::1",
			want: false,
		},

		// Hostnames (not IPs)
		{
			name: "regular hostname",
			host: "example.com",
			want: false,
		},
		{
			name: "fqdn",
			host: "api.example.com",
			want: false,
		},
		{
			name: "localhost-like but not localhost",
			host: "localhost.example.com",
			want: false,
		},
		{
			name: "hostname starting with local",
			host: "local.example.com",
			want: false,
		},

		// Edge cases
		{
			name: "link-local IPv6",
			host: "fe80::1",
			want: false,
		},
		{
			name: "multicast",
			host: "224.0.0.1",
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := isLoopbackHost(tt.host)
			if got != tt.want {
				t.Errorf("isLoopbackHost(%q) = %v, want %v", tt.host, got, tt.want)
			}
		})
	}
}

func TestShouldAppendForwardedPort(t *testing.T) {
	tests := []struct {
		name   string
		port   string
		scheme string
		want   bool
	}{
		// Empty port
		{
			name:   "empty port",
			port:   "",
			scheme: "https",
			want:   false,
		},

		// Invalid port (non-numeric)
		{
			name:   "non-numeric port",
			port:   "abc",
			scheme: "https",
			want:   false,
		},
		{
			name:   "port with letters",
			port:   "80a",
			scheme: "http",
			want:   false,
		},
		{
			name:   "negative port string (Atoi accepts it)",
			port:   "-80",
			scheme: "http",
			want:   true, // strconv.Atoi parses "-80" as -80 (valid int)
		},

		// Default ports that should NOT be appended
		{
			name:   "https with 443",
			port:   "443",
			scheme: "https",
			want:   false,
		},
		{
			name:   "http with 80",
			port:   "80",
			scheme: "http",
			want:   false,
		},

		// Default ports for wrong scheme SHOULD be appended
		{
			name:   "http with 443 (unusual)",
			port:   "443",
			scheme: "http",
			want:   true,
		},
		{
			name:   "https with 80 (unusual)",
			port:   "80",
			scheme: "https",
			want:   true,
		},

		// Non-default ports should be appended
		{
			name:   "https with 8443",
			port:   "8443",
			scheme: "https",
			want:   true,
		},
		{
			name:   "http with 8080",
			port:   "8080",
			scheme: "http",
			want:   true,
		},
		{
			name:   "https with custom port",
			port:   "9443",
			scheme: "https",
			want:   true,
		},
		{
			name:   "pulse default port",
			port:   "7655",
			scheme: "https",
			want:   true,
		},

		// Edge cases
		{
			name:   "port 0",
			port:   "0",
			scheme: "http",
			want:   true,
		},
		{
			name:   "high port number",
			port:   "65535",
			scheme: "https",
			want:   true,
		},
		{
			name:   "empty scheme with non-default port",
			port:   "8080",
			scheme: "",
			want:   true,
		},
		{
			name:   "unknown scheme",
			port:   "443",
			scheme: "wss",
			want:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := shouldAppendForwardedPort(tt.port, tt.scheme)
			if got != tt.want {
				t.Errorf("shouldAppendForwardedPort(%q, %q) = %v, want %v", tt.port, tt.scheme, got, tt.want)
			}
		})
	}
}

func TestCanCapturePublicURL_NilInputs(t *testing.T) {
	t.Parallel()

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	cfg := &config.Config{}

	tests := []struct {
		name string
		cfg  *config.Config
		req  *http.Request
		want bool
	}{
		{
			name: "nil config",
			cfg:  nil,
			req:  req,
			want: false,
		},
		{
			name: "nil request",
			cfg:  cfg,
			req:  nil,
			want: false,
		},
		{
			name: "both nil",
			cfg:  nil,
			req:  nil,
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := canCapturePublicURL(tt.cfg, tt.req)
			if got != tt.want {
				t.Errorf("canCapturePublicURL() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestCapturePublicURLFromRequest_NilInputs(t *testing.T) {
	t.Parallel()

	t.Run("nil router", func(t *testing.T) {
		var r *Router
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		// Should not panic
		r.capturePublicURLFromRequest(req)
	})

	t.Run("nil request", func(t *testing.T) {
		r := &Router{config: &config.Config{}}
		// Should not panic
		r.capturePublicURLFromRequest(nil)
	})

	t.Run("nil config", func(t *testing.T) {
		r := &Router{config: nil}
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		// Should not panic
		r.capturePublicURLFromRequest(req)
	})
}

func TestHostAgentSearchCandidates(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		platform string
		arch     string
		wantLen  int // expected number of search paths
	}{
		{
			name:     "strict mode with both params",
			platform: "linux",
			arch:     "amd64",
			wantLen:  3, // 3 paths with platform-arch suffix
		},
		{
			name:     "platform only, no arch",
			platform: "linux",
			arch:     "",
			wantLen:  3, // 3 paths with platform suffix only
		},
		{
			name:     "no params returns generic paths",
			platform: "",
			arch:     "",
			wantLen:  3, // 3 generic paths
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			paths := hostAgentSearchCandidates(tt.platform, tt.arch)
			if len(paths) != tt.wantLen {
				t.Errorf("hostAgentSearchCandidates(%q, %q) returned %d paths, want %d",
					tt.platform, tt.arch, len(paths), tt.wantLen)
			}
		})
	}
}
