package httpserver_test

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"testing"
)

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

	router, _, store := newTestRouterWithStore(t)
	sessionCookie := loginAsAdmin(t, router)

	projectLocation := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Desktop App"},
		"slug": {"desktop-app"},
	}, http.StatusSeeOther, sessionCookie)
	projectID := extractResourceID(t, projectLocation, "/admin/projects/")

	submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Mobile App"},
		"slug": {"mobile-app"},
	}, http.StatusSeeOther, sessionCookie)

	tagLocation := submitForm(t, router, http.MethodPost, "/admin/tags", url.Values{
		"name": {"Windows"},
		"slug": {"windows"},
	}, http.StatusSeeOther, sessionCookie)
	tagID := extractResourceID(t, tagLocation, "/admin/tags/")

	submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID+"/tags", url.Values{
		"tag_id": {tagID},
	}, http.StatusSeeOther, sessionCookie)

	csrfCookie := ensureCSRFCookie(t, router, sessionCookie)
	recorder := performRequest(t, router, http.MethodPost, "/admin/api-keys", url.Values{
		"name":                {"Windows Clients"},
		"description":         {"Download access for Windows builds."},
		"scope_mode":          {"tag_allow_list"},
		"can_download":        {"1"},
		"can_manage_projects": {"1"},
		"tag_id":              {tagID},
		"csrf_token":          {csrfCookie.Value},
	}, sessionCookie, csrfCookie)

	if recorder.Code != http.StatusOK {
		t.Fatalf("expected create api key page, got %d with body %s", recorder.Code, recorder.Body.String())
	}

	assertHeaderContains(t, recorder, "Cache-Control", "no-store")
	assertHeaderEquals(t, recorder, "Pragma", "no-cache")
	assertHeaderEquals(t, recorder, "Expires", "0")

	body := recorder.Body.String()
	rawKey := findRawAPIKey(t, body)
	if !strings.Contains(body, rawKey) {
		t.Fatal("expected raw api key to be shown on creation response")
	}

	assertBodyContains(t, body, `value="Windows Clients"`)
	assertBodyContains(t, body, "Download access for Windows builds.")
	assertBodyContains(t, body, `option value="tag_allow_list" selected`)
	assertBodyContains(t, body, `name="can_download" value="1" checked`)
	assertBodyContains(t, body, `name="can_manage_projects" value="1" checked`)
	assertBodyContains(t, body, `name="tag_id" value="`+tagID+`" checked`)
	assertBodyContains(t, body, "/api/v1/projects")
	assertBodyContains(t, body, "/api/v1/projects/desktop-app/releases/latest")
	assertBodyContains(t, body, "Authorization: Bearer")

	keys, err := store.APIKeys.List(context.Background())
	if err != nil {
		t.Fatalf("list api keys: %v", err)
	}

	if len(keys) != 1 {
		t.Fatalf("expected one api key, got %d", len(keys))
	}

	keyID := keys[0].APIKey.ID
	storedKey, err := store.APIKeys.GetByID(context.Background(), keyID)
	if err != nil {
		t.Fatalf("load stored api key: %v", err)
	}

	if storedKey.KeyHash == rawKey {
		t.Fatal("expected stored api key hash to differ from the raw key")
	}

	accessibleProjects, err := store.APIKeys.ListAccessibleProjects(context.Background(), keyID, storedKey.ScopeMode)
	if err != nil {
		t.Fatalf("list accessible projects: %v", err)
	}

	if len(accessibleProjects) != 1 || accessibleProjects[0].Name != "Desktop App" {
		t.Fatalf("expected only Desktop App to be accessible, got %+v", accessibleProjects)
	}

	recorder = performRequest(t, router, http.MethodGet, "/admin/api-keys/"+itoa64(keyID), nil, sessionCookie)
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected api key detail page to load, got %d", recorder.Code)
	}

	if strings.Contains(recorder.Body.String(), rawKey) {
		t.Fatal("expected raw api key to disappear after the creation response")
	}

	submitForm(t, router, http.MethodPost, "/admin/api-keys/"+itoa64(keyID)+"/activate", url.Values{
		"state": {"revoke"},
	}, http.StatusSeeOther, sessionCookie)

	storedKey, err = store.APIKeys.GetByID(context.Background(), keyID)
	if err != nil {
		t.Fatalf("reload revoked api key: %v", err)
	}

	if storedKey.IsActive {
		t.Fatal("expected api key to be revoked")
	}
}

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

	router, _, _ := newTestRouterWithStore(t)
	sessionCookie := loginAsAdmin(t, router)

	projectLocation := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Desktop App"},
		"slug": {"desktop-app"},
	}, http.StatusSeeOther, sessionCookie)
	projectID := extractResourceID(t, projectLocation, "/admin/projects/")

	submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Mobile App"},
		"slug": {"mobile-app"},
	}, http.StatusSeeOther, sessionCookie)

	csrfCookie := ensureCSRFCookie(t, router, sessionCookie)
	recorder := performRequest(t, router, http.MethodPost, "/admin/api-keys", url.Values{
		"name":         {"Desktop Only"},
		"description":  {"Project allow-list clients."},
		"scope_mode":   {"project_allow_list"},
		"can_download": {"1"},
		"project_id":   {projectID},
		"csrf_token":   {csrfCookie.Value},
	}, sessionCookie, csrfCookie)

	if recorder.Code != http.StatusOK {
		t.Fatalf("expected create api key page, got %d with body %s", recorder.Code, recorder.Body.String())
	}

	body := recorder.Body.String()
	assertBodyContains(t, body, `value="Desktop Only"`)
	assertBodyContains(t, body, "Project allow-list clients.")
	assertBodyContains(t, body, `option value="project_allow_list" selected`)
	assertBodyContains(t, body, `name="project_id" value="`+projectID+`" checked`)
	assertBodyContains(t, body, "/api/v1/projects")
	assertBodyContains(t, body, "/api/v1/projects/desktop-app/releases/latest")
	assertHeaderContains(t, recorder, "Cache-Control", "no-store")
}

func findRawAPIKey(t *testing.T, body string) string {
	t.Helper()

	re := regexp.MustCompile(`upsk_[A-Za-z0-9_-]+`)
	matches := re.FindAllString(body, -1)
	if len(matches) == 0 {
		t.Fatal("expected raw api key in response body")
	}

	longest := matches[0]
	for _, match := range matches[1:] {
		if len(match) > len(longest) {
			longest = match
		}
	}

	return longest
}

func itoa64(value int64) string {
	return strconv.FormatInt(value, 10)
}

func assertBodyContains(t *testing.T, body, fragment string) {
	t.Helper()

	if !strings.Contains(body, fragment) {
		t.Fatalf("expected response body to contain %q", fragment)
	}
}

func assertHeaderContains(t *testing.T, recorder *httptest.ResponseRecorder, key, fragment string) {
	t.Helper()

	if value := recorder.Header().Get(key); !strings.Contains(value, fragment) {
		t.Fatalf("expected %s header to contain %q, got %q", key, fragment, value)
	}
}

func assertHeaderEquals(t *testing.T, recorder *httptest.ResponseRecorder, key, want string) {
	t.Helper()

	if value := recorder.Header().Get(key); value != want {
		t.Fatalf("expected %s header %q, got %q", key, want, value)
	}
}
