package main

import (
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestSubjectsReceiveStableIsolatedSlots(t *testing.T) {
	statePath := filepath.Join(t.TempDir(), "tenants.json")
	router, err := newTenantRouter(statePath, 2, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	first, err := router.slotFor("keycloak-subject-a")
	if err != nil {
		t.Fatal(err)
	}
	again, _ := router.slotFor("keycloak-subject-a")
	second, err := router.slotFor("keycloak-subject-b")
	if err != nil {
		t.Fatal(err)
	}
	if first != again || first == second {
		t.Fatalf("unexpected slots: first=%d again=%d second=%d", first, again, second)
	}
	if _, err := router.slotFor("keycloak-subject-c"); err == nil {
		t.Fatal("expected the fixed private tenant pool to report capacity")
	}
	reloaded, err := newTenantRouter(statePath, 2, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	persisted, _ := reloaded.slotFor("keycloak-subject-a")
	if persisted != first {
		t.Fatalf("assignment changed after reload: %d != %d", persisted, first)
	}
}

func TestRouterBlocksAdministrationAndRequiresIdentity(t *testing.T) {
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "http://127.0.0.1" })
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		method string
		path   string
		user   string
		want   int
	}{
		{http.MethodGet, "/", "", http.StatusUnauthorized},
		{http.MethodPost, "/api/providers", "subject", http.StatusForbidden},
		{http.MethodPost, "/api/commands/exec", "subject", http.StatusForbidden},
		{http.MethodGet, "/api/dashboard/config", "subject", http.StatusForbidden},
		{http.MethodGet, "/api/env", "subject", http.StatusForbidden},
	} {
		request := httptest.NewRequest(test.method, test.path, nil)
		request.Header.Set("X-Forwarded-User", test.user)
		response := httptest.NewRecorder()
		router.ServeHTTP(response, request)
		if response.Code != test.want {
			t.Fatalf("%s %s: got %d, want %d", test.method, test.path, response.Code, test.want)
		}
	}
}

func TestRouterRetiresStaleServiceWorkerWithoutIdentity(t *testing.T) {
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodGet, "/sw.js?v=exp-v0.52.181", nil)
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("got status %d", response.Code)
	}
	if response.Header().Get("Service-Worker-Allowed") != "/" {
		t.Fatal("service worker retirement response does not cover the origin")
	}
	if !strings.Contains(response.Header().Get("Cache-Control"), "no-store") {
		t.Fatal("service worker retirement response is cacheable")
	}
	if !strings.Contains(response.Header().Get("Clear-Site-Data"), "storage") {
		t.Fatal("service worker retirement response does not clear stale storage")
	}
	if !strings.Contains(response.Body.String(), "registration.unregister") {
		t.Fatal("service worker retirement script does not unregister itself")
	}
}

func TestRouterProxiesWebUIAndAddsTelegramShortcut(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if request.Header.Get("X-Forwarded-User") != "" || request.Header.Get("X-Auth-Request-User") != "" {
			t.Fatal("external identity header leaked to tenant backend")
		}
		if request.Header.Get(trustedTenantHeader) != "slot-0" {
			t.Fatalf("trusted tenant identity was not asserted: %q", request.Header.Get(trustedTenantHeader))
		}
		cookies := request.Cookies()
		if len(cookies) != 2 || cookies[0].Name != tenantSessionCookie || cookies[1].Name != tenantProfileCookie {
			t.Fatalf("unexpected backend cookies: %#v", cookies)
		}
		writer.Header().Set("Content-Type", "text/html; charset=utf-8")
		_, _ = io.WriteString(writer, "<html><head></head><body>Hermes WebUI</body></html>")
	}))
	defer backend.Close()
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set("X-Forwarded-User", "subject")
	request.Header.Set("X-Auth-Request-User", "subject")
	request.Header.Set("Cookie", "_oauth2_proxy=secret; "+tenantSessionCookie+"=session; "+tenantProfileCookie+"=default")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("got status %d", response.Code)
	}
	if !strings.Contains(response.Body.String(), "hermes-chat-bridge.js") || !strings.Contains(response.Body.String(), "hermes-chat-bridge.css") {
		t.Fatal("Telegram shortcut assets were not injected")
	}
	assetRequest := httptest.NewRequest(http.MethodGet, "/hermes-chat-bridge.js", nil)
	assetRequest.Header.Set("X-Forwarded-User", "subject")
	assetResponse := httptest.NewRecorder()
	router.ServeHTTP(assetResponse, assetRequest)
	asset := assetResponse.Body.String()
	if !strings.Contains(asset, "hermes-files-sidebar") || !strings.Contains(asset, "hermes-telegram-sidebar") {
		t.Fatal("Files and Telegram were not integrated into the existing sidebar")
	}
	if !strings.Contains(asset, "document.querySelector('.rail')") || !strings.Contains(asset, "data-tooltip', 'Telegram") {
		t.Fatal("Telegram was not integrated as a native WebUI navigation action")
	}
	if strings.Contains(asset, "if (!files) return") {
		t.Fatal("Telegram navigation still depends on the removed legacy Files link")
	}
	if strings.Contains(asset, "position:fixed") || strings.Contains(asset, "hermes-chat-tools") {
		t.Fatal("legacy floating chat controls remain in the mobile bridge")
	}
}

func TestTelegramPageExplainsOneTimeOperatorSetup(t *testing.T) {
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	request := httptest.NewRequest(http.MethodGet, "/telegram", nil)
	request.Header.Set("X-Forwarded-User", "subject")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("got status %d", response.Code)
	}
	body := response.Body.String()
	for _, expected := range []string{
		"https://t.me/BotFather",
		"https://secret.bstein.dev",
		"vault login -method=oidc",
		"vault kv patch -mount=kv atlas/hermes/chat-telegram",
		"bot_token",
		"relay_key",
		"each Keycloak user",
	} {
		if !strings.Contains(body, expected) {
			t.Fatalf("Telegram operator setup omitted %q", expected)
		}
	}
}

func TestRouterRedirectsNativeLoginToSafeChatDestination(t *testing.T) {
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		path string
		want string
	}{
		{"/login?next=%2Fsession%2F9048e2a574d1", "/session/9048e2a574d1"},
		{"/login?next=%2Fchat%3Fresume%3Dabc", "/chat?resume=abc"},
		{"/login?next=https%3A%2F%2Fevil.example", "/"},
		{"/login?next=%2F%2Fevil.example", "/"},
		{"/login?next=%2Flogin", "/"},
		{"/login?next=%2Flogin%2Fagain", "/"},
	} {
		request := httptest.NewRequest(http.MethodGet, test.path, nil)
		request.Header.Set("X-Forwarded-User", "subject")
		response := httptest.NewRecorder()
		router.ServeHTTP(response, request)
		if response.Code != http.StatusFound {
			t.Fatalf("%s: got status %d", test.path, response.Code)
		}
		if location := response.Header().Get("Location"); location != test.want {
			t.Fatalf("%s: got redirect %q, want %q", test.path, location, test.want)
		}
	}
}

func TestRouterAllowsTenantScopedPersonalization(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.WriteHeader(http.StatusNoContent)
	}))
	defer backend.Close()
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
	if err != nil {
		t.Fatal(err)
	}
	for _, path := range []string{
		"/api/memory",
		"/api/memory/write",
		"/api/skills",
		"/api/skills/save",
		"/api/profiles",
		"/api/profile/create",
	} {
		request := httptest.NewRequest(http.MethodGet, path, nil)
		request.Header.Set("X-Forwarded-User", "subject")
		response := httptest.NewRecorder()
		router.ServeHTTP(response, request)
		if response.Code != http.StatusNoContent {
			t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent)
		}
	}
}

func TestRouterAllowsPrivateWorkspaceReadsButBlocksRegistration(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.WriteHeader(http.StatusNoContent)
	}))
	defer backend.Close()
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
	if err != nil {
		t.Fatal(err)
	}

	for _, path := range []string{
		"/api/workspaces",
		"/api/workspaces/suggest",
		"/api/file",
		"/api/file/raw",
		"/api/folder/download",
		"/api/logs",
		"/api/rollback/list",
		"/api/rollback/diff",
	} {
		request := httptest.NewRequest(http.MethodGet, path, nil)
		request.Header.Set("X-Forwarded-User", "subject")
		response := httptest.NewRecorder()
		router.ServeHTTP(response, request)
		if response.Code != http.StatusNoContent {
			t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent)
		}
	}

	request := httptest.NewRequest(http.MethodPost, "/api/workspaces/add", strings.NewReader(`{"path":"/opt/data"}`))
	request.Header.Set("X-Forwarded-User", "subject")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusForbidden {
		t.Fatalf("workspace registration got %d, want %d", response.Code, http.StatusForbidden)
	}
}

func TestPrivateFileBrowserIsTenantAuthenticatedAndHidesKanban(t *testing.T) {
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	unauthenticated := httptest.NewRequest(http.MethodGet, "/private-files", nil)
	unauthenticatedResponse := httptest.NewRecorder()
	router.ServeHTTP(unauthenticatedResponse, unauthenticated)
	if unauthenticatedResponse.Code != http.StatusUnauthorized {
		t.Fatalf("unauthenticated browser got %d", unauthenticatedResponse.Code)
	}

	request := httptest.NewRequest(http.MethodGet, "/private-files?session_id=session-1", nil)
	request.Header.Set("X-Forwarded-User", "subject")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("got %d", response.Code)
	}
	if !strings.Contains(response.Body.String(), "Private files") || !strings.Contains(response.Body.String(), "data-files-page") {
		t.Fatal("private browser page was not rendered")
	}

	assetRequest := httptest.NewRequest(http.MethodGet, "/hermes-chat-bridge.js", nil)
	assetRequest.Header.Set("X-Forwarded-User", "subject")
	assetResponse := httptest.NewRecorder()
	router.ServeHTTP(assetResponse, assetRequest)
	if !strings.Contains(assetResponse.Body.String(), "endpoint('list'") || !strings.Contains(assetResponse.Body.String(), `'[data-panel="kanban"]'`) {
		t.Fatal("file browsing or chat-only navigation policy is missing")
	}
}

func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodPost {
			t.Fatalf("got method %s", request.Method)
		}
		writer.WriteHeader(http.StatusNoContent)
	}))
	defer backend.Close()
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL })
	if err != nil {
		t.Fatal(err)
	}
	for _, path := range []string{"/api/model/set", "/api/reasoning"} {
		request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
		request.Header.Set("X-Forwarded-User", "subject")
		response := httptest.NewRecorder()
		router.ServeHTTP(response, request)
		if response.Code != http.StatusNoContent {
			t.Fatalf("POST %s: got %d", path, response.Code)
		}
	}
}

func TestTelegramLinkUsesHashedStateAndExpires(t *testing.T) {
	statePath := filepath.Join(t.TempDir(), "state.json")
	router, err := newTenantRouter(statePath, 1, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	now := time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC)
	router.now = func() time.Time { return now }
	code, expires, err := router.createLink("raw-keycloak-subject")
	if err != nil {
		t.Fatal(err)
	}
	if expires.Sub(now) != 10*time.Minute {
		t.Fatalf("unexpected expiry: %s", expires)
	}
	if _, err := router.consumeLink("123456789", code); err != nil {
		t.Fatal(err)
	}
	if _, linked := router.telegramSlot("123456789"); !linked {
		t.Fatal("Telegram identity was not linked")
	}
	content, err := os.ReadFile(statePath)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(content), "raw-keycloak-subject") || strings.Contains(string(content), "123456789") || strings.Contains(string(content), code) {
		t.Fatal("raw identity or one-time code was persisted")
	}
	secondCode, _, err := router.createLink("raw-keycloak-subject")
	if err != nil {
		t.Fatal(err)
	}
	router.now = func() time.Time { return now.Add(11 * time.Minute) }
	if _, err := router.consumeLink("987654321", secondCode); err == nil {
		t.Fatal("expected expired link code to be rejected")
	}
}

func TestTelegramWebActionsRequireExplicitSameOriginHeader(t *testing.T) {
	router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
	if err != nil {
		t.Fatal(err)
	}
	router.telegram = &telegramBot{}
	request := httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`))
	request.Header.Set("X-Forwarded-User", "subject")
	request.Header.Set("Content-Type", "application/json")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusForbidden {
		t.Fatalf("got %d", response.Code)
	}
	request = httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`))
	request.Header.Set("X-Forwarded-User", "subject")
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("X-Hermes-Action", "telegram-link")
	response = httptest.NewRecorder()
	router.ServeHTTP(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("got %d: %s", response.Code, response.Body.String())
	}
}
