package api

import (
	"bytes"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// --- isWebSocketUpgrade tests ---

func TestIsWebSocketUpgrade_ValidUpgrade(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Connection", "Upgrade")
	r.Header.Set("Upgrade", "websocket")
	if !isWebSocketUpgrade(r) {
		t.Error("expected true for valid WebSocket upgrade")
	}
}

func TestIsWebSocketUpgrade_CaseInsensitive(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Connection", "upgrade")
	r.Header.Set("Upgrade", "WebSocket")
	if !isWebSocketUpgrade(r) {
		t.Error("expected true for case-insensitive upgrade headers")
	}
}

func TestIsWebSocketUpgrade_MissingConnectionHeader(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Upgrade", "websocket")
	if isWebSocketUpgrade(r) {
		t.Error("expected false when Connection header is missing")
	}
}

func TestIsWebSocketUpgrade_MissingUpgradeHeader(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Connection", "Upgrade")
	if isWebSocketUpgrade(r) {
		t.Error("expected false when Upgrade header is missing")
	}
}

func TestIsWebSocketUpgrade_NonWebsocketUpgrade(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Connection", "Upgrade")
	r.Header.Set("Upgrade", "h2c")
	if isWebSocketUpgrade(r) {
		t.Error("expected false for non-websocket upgrade")
	}
}

func TestIsWebSocketUpgrade_ConnectionKeepAlive(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Connection", "keep-alive")
	r.Header.Set("Upgrade", "websocket")
	if isWebSocketUpgrade(r) {
		t.Error("expected false when Connection is keep-alive")
	}
}

// --- isOriginAllowed tests ---

func TestIsOriginAllowed_EmptyOriginAllowed(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	// No Origin header = same-origin or non-browser
	if !isOriginAllowed(r, []string{"https://example.com"}) {
		t.Error("expected true when no Origin header (same-origin)")
	}
}

// Note: fixtures use origins distinct from httptest's default request host
// (example.com) so these exercise the allowlist path, not the same-origin
// allowance.
func TestIsOriginAllowed_MatchingOrigin(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Origin", "https://app.example.net")
	if !isOriginAllowed(r, []string{"https://app.example.net"}) {
		t.Error("expected true for matching origin")
	}
}

func TestIsOriginAllowed_CaseInsensitive(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Origin", "HTTPS://APP.EXAMPLE.NET")
	if !isOriginAllowed(r, []string{"https://app.example.net"}) {
		t.Error("expected true for case-insensitive origin match")
	}
}

func TestIsOriginAllowed_NonMatchingOrigin(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Origin", "https://evil.com")
	if isOriginAllowed(r, []string{"https://example.com"}) {
		t.Error("expected false for non-matching origin")
	}
}

func TestIsOriginAllowed_WildcardAllowsAll(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Origin", "https://anything.com")
	if !isOriginAllowed(r, []string{"*"}) {
		t.Error("expected true for wildcard origin")
	}
}

func TestIsOriginAllowed_EmptyAllowlistDenies(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Origin", "https://cross.example.net")
	if isOriginAllowed(r, []string{}) {
		t.Error("expected false when allowlist is empty")
	}
}

func TestIsOriginAllowed_MultipleAllowedOrigins(t *testing.T) {
	r := httptest.NewRequest("GET", "/livekit/", nil)
	r.Header.Set("Origin", "https://b.com")
	allowed := []string{"https://a.com", "https://b.com", "https://c.com"}
	if !isOriginAllowed(r, allowed) {
		t.Error("expected true for origin in multi-origin allowlist")
	}
}

// The desktop client's webview issues LiveKit signal/validate requests
// directly (not via its Origin-stripping Rust proxy), so its fixed origins
// must pass even on the default empty allowlist — otherwise voice 403s on
// every fresh install for any client not on the server machine.
func TestIsOriginAllowed_FirstPartyDesktopOrigins(t *testing.T) {
	for _, origin := range []string{
		"http://tauri.localhost",  // WebView2 (Windows)
		"https://tauri.localhost", // WebView2, https variant
		"tauri://localhost",       // WKWebView / WebKitGTK (macOS, Linux)
		"HTTP://TAURI.LOCALHOST",  // case-insensitive
	} {
		r := httptest.NewRequest("GET", "/livekit/", nil)
		r.Header.Set("Origin", origin)
		if !isOriginAllowed(r, nil) {
			t.Errorf("expected true for first-party desktop origin %q with empty allowlist", origin)
		}
	}
}

// A browser always attaches the page's origin to WebSocket handshakes, so a
// page served by this very server (Origin host == request Host) must pass
// even on the default empty allowlist. This mirrors websocket.Accept's
// default same-origin policy, which the chat WS endpoint already applies —
// without it a same-origin client can chat but voice 403s.
func TestIsOriginAllowed_SameOriginAllowed(t *testing.T) {
	for _, origin := range []string{
		"https://192.168.0.125:8443",
		"HTTPS://192.168.0.125:8443", // case-insensitive
	} {
		r := httptest.NewRequest("GET", "https://192.168.0.125:8443/livekit/", nil)
		r.Header.Set("Origin", origin)
		if !isOriginAllowed(r, nil) {
			t.Errorf("expected true for same-origin %q with empty allowlist", origin)
		}
	}
}

// Same host but a different port is a different origin and must not pass.
func TestIsOriginAllowed_SameHostDifferentPortDenied(t *testing.T) {
	r := httptest.NewRequest("GET", "https://192.168.0.125:8443/livekit/", nil)
	r.Header.Set("Origin", "https://192.168.0.125:9999")
	if isOriginAllowed(r, nil) {
		t.Error("expected false for same host with different port")
	}
}

// A lookalike origin must NOT ride along with the first-party allowance.
func TestIsOriginAllowed_FirstPartyLookalikesDenied(t *testing.T) {
	for _, origin := range []string{
		"http://tauri.localhost.evil.com",
		"http://eviltauri.localhost",
		"http://tauri.localhost:8080",
		"tauri://evil",
	} {
		r := httptest.NewRequest("GET", "/livekit/", nil)
		r.Header.Set("Origin", origin)
		if isOriginAllowed(r, nil) {
			t.Errorf("expected false for lookalike origin %q", origin)
		}
	}
}

// --- NewLiveKitProxy HTTP routing tests ---

func TestLiveKitProxy_BlocksAdminPath(t *testing.T) {
	proxy := NewLiveKitProxy("http://localhost:7880", []string{"*"})
	r := httptest.NewRequest("GET", "/admin/dashboard", nil)
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for /admin path, got %d", w.Code)
	}
}

func TestLiveKitProxy_BlocksMetricsPath(t *testing.T) {
	proxy := NewLiveKitProxy("http://localhost:7880", []string{"*"})
	r := httptest.NewRequest("GET", "/metrics", nil)
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for /metrics path, got %d", w.Code)
	}
}

func TestLiveKitProxy_BlocksDebugPath(t *testing.T) {
	proxy := NewLiveKitProxy("http://localhost:7880", []string{"*"})
	r := httptest.NewRequest("GET", "/debug/pprof", nil)
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for /debug path, got %d", w.Code)
	}
}

func TestLiveKitProxy_BlocksTwirpPath(t *testing.T) {
	proxy := NewLiveKitProxy("http://localhost:7880", []string{"*"})
	r := httptest.NewRequest("POST", "/twirp/livekit.RoomService/ListRooms", nil)
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for /twirp path, got %d", w.Code)
	}
}

func TestLiveKitProxy_AllowsNormalPath(t *testing.T) {
	// Use a test backend that returns 200
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK")) //nolint:errcheck
	}))
	defer backend.Close()

	proxy := NewLiveKitProxy(backend.URL, []string{"*"})
	r := httptest.NewRequest("GET", "/rtc", nil)
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for normal path, got %d", w.Code)
	}
}

func TestLiveKitProxy_BlocksCrossOriginHTTP(t *testing.T) {
	proxy := NewLiveKitProxy("http://localhost:7880", []string{"https://myapp.com"})
	r := httptest.NewRequest("GET", "/rtc", nil)
	r.Header.Set("Origin", "https://evil.com")
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for cross-origin HTTP, got %d", w.Code)
	}
}

func TestLiveKitProxy_AllowsSameOriginHTTP(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer backend.Close()

	proxy := NewLiveKitProxy(backend.URL, []string{"https://myapp.com"})
	r := httptest.NewRequest("GET", "/rtc", nil)
	r.Header.Set("Origin", "https://myapp.com")
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for same-origin HTTP, got %d", w.Code)
	}
}

func TestLiveKitProxy_DoesNotBlockUserMetrics(t *testing.T) {
	// "/user-metrics" should NOT be blocked (only "/metrics" segment is blocked)
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer backend.Close()

	proxy := NewLiveKitProxy(backend.URL, []string{"*"})
	r := httptest.NewRequest("GET", "/user-metrics", nil)
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)
	// The path is split by "/" — "user-metrics" is one segment, not "metrics"
	// Wait — actually the blocked check splits by "/" and checks each segment.
	// "/user-metrics" splits to ["", "user-metrics"] so "user-metrics" != "metrics". Should pass.
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for /user-metrics (not an exact segment match), got %d", w.Code)
	}
}

// TestProxyWebSocket_DialFailureDoesNotLogAccessToken locks the credential out
// of the log stream: websocket.Dial wraps a *url.Error carrying the full
// backend URL, so a dial failure (LiveKit down, restarting, refused) used to
// print the caller's live room-join JWT at Warn level — replayable inside its
// 5-minute TTL by anyone reading stdout or the admin panel's log ring buffer.
func TestProxyWebSocket_DialFailureDoesNotLogAccessToken(t *testing.T) {
	const token = "eyJhbGciOiJIUzI1NiJ9.SECRET-LIVEKIT-JWT-PAYLOAD.c2lnbmF0dXJl"

	var logs bytes.Buffer
	prev := slog.Default()
	slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})))
	t.Cleanup(func() { slog.SetDefault(prev) })

	// Port 1 on loopback refuses connections, which is the exact branch where
	// the wrapped *url.Error carries the query string.
	proxy := NewLiveKitProxy("http://127.0.0.1:1", []string{"*"})
	r := httptest.NewRequest("GET", "/rtc?access_token="+token, nil)
	r.Header.Set("Connection", "Upgrade")
	r.Header.Set("Upgrade", "websocket")
	w := httptest.NewRecorder()
	proxy.ServeHTTP(w, r)

	if w.Code != http.StatusBadGateway {
		t.Fatalf("expected 502 when the backend refuses the dial, got %d", w.Code)
	}
	out := logs.String()
	if out == "" {
		t.Fatal("expected the dial failure to be logged at all")
	}
	if strings.Contains(out, token) {
		t.Fatalf("the LiveKit access token leaked into the log stream:\n%s", out)
	}
	if !strings.Contains(out, "backend dial failed") {
		t.Errorf("the failure must still be diagnosable, got:\n%s", out)
	}
}

func TestLiveKitProxy_InvalidURL_FallsBackToLocalhost(t *testing.T) {
	// Should not panic with invalid URL
	proxy := NewLiveKitProxy("://invalid", []string{"*"})
	r := httptest.NewRequest("GET", "/rtc", nil)
	w := httptest.NewRecorder()
	// This will fail to connect to localhost:7880, but should not panic
	proxy.ServeHTTP(w, r)
	// We expect a 502 or similar because localhost:7880 isn't running
	if w.Code == http.StatusOK {
		t.Error("expected non-200 for invalid backend URL fallback")
	}
}
