package middleware

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
)

const testSessionControlProof = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"

func TestNewSessionControlAdmissionRejectsInvalidConfiguration(t *testing.T) {
	for _, origins := range [][]string{
		nil,
		{""},
		{"http://app.example"},
		{"https://app.example/path"},
		{"https://app.example", "not-an-origin"},
	} {
		if _, err := NewSessionControlAdmission(origins...); err == nil {
			t.Fatalf("NewSessionControlAdmission(%q) succeeded, want error", origins)
		}
	}

	if _, err := NewSessionControlAdmission("https://app.example", "http://localhost:5173"); err != nil {
		t.Fatalf("valid session-control origins rejected: %v", err)
	}
}

func TestSessionControlHTTPAdmissionUsesProtectedCredentialBoundary(t *testing.T) {
	admission, err := NewSessionControlHTTPAdmission("https://app.example")
	if err != nil {
		t.Fatalf("NewSessionControlHTTPAdmission failed: %v", err)
	}

	expiresAt := time.Now().Add(time.Hour)
	var admitted bool
	handler := admission(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		admitted = SessionControlRequestAdmitted(r.Context())
		if err := IssueSessionControlResponse(r.Context(), testSessionControlProof, expiresAt); err != nil {
			t.Fatalf("IssueSessionControlResponse failed: %v", err)
		}
		w.WriteHeader(http.StatusNoContent)
	}))

	request := admittedSessionControlRequestFor(http.MethodPost, "/services/oauth2/token")
	response := httptest.NewRecorder()
	ProtectSessionCredentialResponses(handler).ServeHTTP(response, request)

	if response.Code != http.StatusNoContent || !admitted {
		t.Fatalf("response=(status=%d admitted=%v), want 204 and admitted", response.Code, admitted)
	}
	var controlCookies []*http.Cookie
	for _, cookie := range response.Result().Cookies() {
		if cookie.Name == SessionControlCookieName {
			controlCookies = append(controlCookies, cookie)
		}
	}
	if len(controlCookies) != 1 {
		t.Fatalf("session-control cookies=%#v, want exactly one", controlCookies)
	}
	assertSessionControlCookie(t, controlCookies[0], testSessionControlProof)
}

func TestIssueRenewedSessionResponsePublishesOnlyTheProtectedPair(t *testing.T) {
	admission, err := NewSessionControlHTTPAdmission("https://app.example")
	if err != nil {
		t.Fatalf("NewSessionControlHTTPAdmission failed: %v", err)
	}

	handler := admission(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := IssueRenewedSessionResponse(
			r.Context(), "replacement-bearer", testSessionControlProof, time.Now().Add(time.Hour),
		); err != nil {
			t.Fatalf("IssueRenewedSessionResponse failed: %v", err)
		}
		w.WriteHeader(http.StatusNoContent)
	}))
	request := admittedSessionControlRequestFor(http.MethodGet, "/services/data/v63.0/query")
	request.AddCookie(&http.Cookie{Name: SessionControlCookieName, Value: testSessionControlProof})
	response := httptest.NewRecorder()
	ProtectSessionCredentialResponses(handler).ServeHTTP(response, request)

	if got := response.Header().Get(HeaderRefreshedToken); got != "replacement-bearer" {
		t.Fatalf("%s=%q, want replacement bearer", HeaderRefreshedToken, got)
	}
	var controlCookies []*http.Cookie
	for _, cookie := range response.Result().Cookies() {
		if cookie.Name == SessionControlCookieName {
			controlCookies = append(controlCookies, cookie)
		}
	}
	if len(controlCookies) != 1 {
		t.Fatalf("session-control cookies=%#v, want exactly one", controlCookies)
	}
	assertSessionControlCookie(t, controlCookies[0], testSessionControlProof)
}

func TestSessionControlAdmissionExposesProofOnlyForAdmittedBrowserRequests(t *testing.T) {
	tests := []struct {
		name         string
		method       string
		origin       string
		referer      string
		fetchSite    string
		intent       string
		cookies      []string
		extraHeaders http.Header
		wantProof    bool
	}{
		{
			name:      "same-origin safe request with exact origin",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			wantProof: true,
		},
		{
			name:      "same-origin safe request with exact referer",
			method:    http.MethodGet,
			referer:   "https://app.example/records/001?tab=details",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			wantProof: true,
		},
		{
			name:      "same-origin safe request without source header",
			method:    http.MethodGet,
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			wantProof: true,
		},
		{
			name:      "same-site split frontend with exact origin",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameSite,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			wantProof: true,
		},
		{
			name:      "unsafe request with exact origin",
			method:    http.MethodPost,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			wantProof: true,
		},
		{
			name:      "unsafe request cannot rely on referer",
			method:    http.MethodPost,
			referer:   "https://app.example/logout",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "same-site request requires origin",
			method:    http.MethodGet,
			referer:   "https://app.example/records",
			fetchSite: secFetchSiteSameSite,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "missing intent",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "wrong intent",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    "renew",
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "cross-site fetch",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: "cross-site",
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "untrusted origin",
			method:    http.MethodGet,
			origin:    "https://evil.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "untrusted referer conflicts with trusted origin",
			method:    http.MethodGet,
			origin:    "https://app.example",
			referer:   "https://evil.example/",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
		},
		{
			name:      "duplicate cookies",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof, strings.Repeat("z", minSessionControlProofBytes)},
		},
		{
			name:      "oversized cookie",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{strings.Repeat("z", maxSessionControlProofBytes+1)},
		},
		{
			name:      "duplicate intent headers",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			extraHeaders: http.Header{
				HeaderSessionControlIntent: []string{SessionControlIntentValue},
			},
		},
		{
			name:      "duplicate origin headers",
			method:    http.MethodGet,
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			cookies:   []string{testSessionControlProof},
			extraHeaders: http.Header{
				headerOrigin: []string{"https://app.example"},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, "/control", nil)
			if tc.origin != "" {
				req.Header.Add(headerOrigin, tc.origin)
			}
			if tc.referer != "" {
				req.Header.Add(headerReferer, tc.referer)
			}
			if tc.fetchSite != "" {
				req.Header.Add(headerSecFetchSite, tc.fetchSite)
			}
			if tc.intent != "" {
				req.Header.Add(HeaderSessionControlIntent, tc.intent)
			}
			for name, values := range tc.extraHeaders {
				for _, value := range values {
					req.Header.Add(name, value)
				}
			}
			for _, proof := range tc.cookies {
				req.AddCookie(&http.Cookie{Name: SessionControlCookieName, Value: proof})
			}

			proof, ok := serveSessionControlRequest(t, req)
			if ok != tc.wantProof {
				t.Fatalf("proof admitted = %v (%q), want %v", ok, proof, tc.wantProof)
			}
			if tc.wantProof && proof != testSessionControlProof {
				t.Fatalf("proof = %q, want %q", proof, testSessionControlProof)
			}
		})
	}
}

func TestSessionControlAdmissionMarksInitialLoginWithoutProof(t *testing.T) {
	tests := []struct {
		name         string
		origin       string
		fetchSite    string
		intent       string
		extraHeaders http.Header
		wantAdmitted bool
	}{
		{
			name:         "first-party unsafe login",
			origin:       "https://app.example",
			fetchSite:    secFetchSiteSameOrigin,
			intent:       SessionControlIntentValue,
			wantAdmitted: true,
		},
		{
			name:      "missing intent",
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
		},
		{
			name:      "wrong intent",
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    "renew",
		},
		{
			name:      "cross-site fetch",
			origin:    "https://app.example",
			fetchSite: "cross-site",
			intent:    SessionControlIntentValue,
		},
		{
			name:      "untrusted origin",
			origin:    "https://evil.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
		},
		{
			name:      "missing unsafe origin",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
		},
		{
			name:      "duplicate origin",
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			extraHeaders: http.Header{
				headerOrigin: []string{"https://app.example"},
			},
		},
		{
			name:      "duplicate intent",
			origin:    "https://app.example",
			fetchSite: secFetchSiteSameOrigin,
			intent:    SessionControlIntentValue,
			extraHeaders: http.Header{
				HeaderSessionControlIntent: []string{SessionControlIntentValue},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, "/control", nil)
			if tc.origin != "" {
				req.Header.Add(headerOrigin, tc.origin)
			}
			if tc.fetchSite != "" {
				req.Header.Add(headerSecFetchSite, tc.fetchSite)
			}
			if tc.intent != "" {
				req.Header.Add(HeaderSessionControlIntent, tc.intent)
			}
			for name, values := range tc.extraHeaders {
				for _, value := range values {
					req.Header.Add(name, value)
				}
			}

			proof, proofOK, admitted := serveSessionControlRequestState(t, req)
			if admitted != tc.wantAdmitted {
				t.Fatalf("request admitted = %v, want %v", admitted, tc.wantAdmitted)
			}
			if proofOK || proof != "" {
				t.Fatalf("initial request exposed proof %q (ok=%v), want none", proof, proofOK)
			}
		})
	}
}

func TestSessionControlAdmissionWithholdsQuotedAndMalformedProofs(t *testing.T) {
	for _, cookieHeader := range []string{
		SessionControlCookieName + `="` + testSessionControlProof + `"`,
		SessionControlCookieName + `=invalid\\proof`,
		SessionControlCookieName + `=short`,
		SessionControlCookieName + `=` + testSessionControlProof + `; ` + SessionControlCookieName + `=invalid\\proof`,
		SessionControlCookieName + `=` + strings.Repeat("x", maxSessionControlProofBytes+1),
	} {
		req := admittedSessionControlRequest(http.MethodGet)
		req.Header.Set("Cookie", cookieHeader)
		proof, ok, admitted, attempted := serveSessionControlRequestControlState(t, req)
		if ok {
			t.Fatalf("Cookie %q exposed proof %q", cookieHeader, proof)
		}
		if admitted || !attempted {
			t.Fatalf("Cookie %q state admitted=%v attempted=%v, want false and true", cookieHeader, admitted, attempted)
		}
	}
}

func TestSessionControlAdmissionSupportsLoopbackFrontendAPISplitWithoutWeakeningCookie(t *testing.T) {
	gin.SetMode(gin.TestMode)
	admission, err := NewSessionControlAdmission("http://localhost:5177")
	if err != nil {
		t.Fatalf("NewSessionControlAdmission failed: %v", err)
	}
	var proof string
	router := gin.New()
	router.GET("/control", admission, func(c *gin.Context) {
		proof, _ = SessionControlProof(c.Request.Context())
		c.Status(http.StatusNoContent)
	})
	req := httptest.NewRequest(http.MethodGet, "/control", nil)
	req.Header.Set(HeaderSessionControlIntent, SessionControlIntentValue)
	req.Header.Set(headerSecFetchSite, secFetchSiteSameSite)
	req.Header.Set(headerOrigin, "http://localhost:5177")
	req.AddCookie(&http.Cookie{Name: SessionControlCookieName, Value: testSessionControlProof})
	router.ServeHTTP(httptest.NewRecorder(), req)

	if proof != testSessionControlProof {
		t.Fatalf("loopback proof = %q, want %q", proof, testSessionControlProof)
	}
	cookie := sessionControlCookie(testSessionControlProof, time.Now().Add(time.Hour), 3600)
	if !cookie.Secure {
		t.Fatal("loopback support weakened the Secure cookie contract")
	}
}

func TestIssueSessionControlCookieUsesProtectedPersistentContract(t *testing.T) {
	gin.SetMode(gin.TestMode)
	expiresAt := time.Now().Add(2 * time.Hour).Truncate(time.Second)
	admission, err := NewSessionControlAdmission("https://app.example")
	if err != nil {
		t.Fatalf("NewSessionControlAdmission failed: %v", err)
	}
	var issueErr error
	router := gin.New()
	router.POST("/issue", admission, func(c *gin.Context) {
		issueErr = IssueSessionControlCookie(c, testSessionControlProof, expiresAt)
		c.Writer.Header().Add("Set-Cookie", SessionControlCookieName+"=downstream-spoof; Path=/")
		c.Writer.Header().Add("Set-Cookie", "other-cookie=preserved; Path=/")
		c.Header(headerCacheControl, "public, max-age=3600")
		c.Status(http.StatusNoContent)
	})
	rec := httptest.NewRecorder()
	ProtectSessionCredentialResponses(router).ServeHTTP(rec, admittedSessionControlRequestFor(http.MethodPost, "/issue"))
	if issueErr != nil {
		t.Fatalf("IssueSessionControlCookie failed: %v", issueErr)
	}

	var controlCookies []*http.Cookie
	for _, cookie := range rec.Result().Cookies() {
		if cookie.Name == SessionControlCookieName {
			controlCookies = append(controlCookies, cookie)
		}
	}
	if len(controlCookies) != 1 {
		t.Fatalf("issued control cookies = %#v, want exactly one protected value", controlCookies)
	}
	assertSessionControlCookie(t, controlCookies[0], testSessionControlProof)
	if !controlCookies[0].Expires.Equal(expiresAt) || controlCookies[0].MaxAge <= 0 {
		t.Fatalf("issued cookie expiry = %v max-age=%d, want expiry %v and positive max-age", controlCookies[0].Expires, controlCookies[0].MaxAge, expiresAt)
	}
	if strings.Contains(strings.ToLower(rec.Header().Get("Set-Cookie")), "domain=") {
		t.Fatalf("issued cookie unexpectedly has Domain: %s", rec.Header().Get("Set-Cookie"))
	}
	if got := rec.Header().Get(headerCacheControl); got != cacheControlNoStore {
		t.Fatalf("%s = %q, want %q", headerCacheControl, got, cacheControlNoStore)
	}
	if !strings.Contains(strings.Join(rec.Header().Values("Set-Cookie"), "\n"), "other-cookie=preserved") {
		t.Fatalf("unrelated Set-Cookie was not preserved: %q", rec.Header().Values("Set-Cookie"))
	}
}

func TestClearSessionControlCookieUsesProtectedResponseBoundary(t *testing.T) {
	gin.SetMode(gin.TestMode)
	var clearErr error
	router := gin.New()
	router.POST("/logout", func(c *gin.Context) {
		clearErr = ClearSessionControlCookie(c)
		c.Status(http.StatusNoContent)
	})
	rec := httptest.NewRecorder()
	ProtectSessionCredentialResponses(router).ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/logout", nil))
	if clearErr != nil {
		t.Fatalf("ClearSessionControlCookie failed: %v", clearErr)
	}
	cleared := rec.Result().Cookies()
	if len(cleared) != 1 {
		t.Fatalf("cleared cookies = %d, want 1", len(cleared))
	}
	assertSessionControlCookie(t, cleared[0], "")
	if cleared[0].MaxAge >= 0 || !cleared[0].Expires.Before(time.Now()) {
		t.Fatalf("cleared cookie expiry = %v max-age=%d, want expired", cleared[0].Expires, cleared[0].MaxAge)
	}
	if got := rec.Header().Get(headerCacheControl); got != cacheControlNoStore {
		t.Fatalf("%s = %q, want %q", headerCacheControl, got, cacheControlNoStore)
	}
}

func TestSessionControlCookieHelpersRequireUncommittedResponseGuard(t *testing.T) {
	gin.SetMode(gin.TestMode)
	expiresAt := time.Now().Add(time.Hour)
	admission, err := NewSessionControlAdmission("https://app.example")
	if err != nil {
		t.Fatalf("NewSessionControlAdmission failed: %v", err)
	}

	var noGuardErr error
	noGuardRouter := gin.New()
	noGuardRouter.POST("/issue", admission, func(c *gin.Context) {
		noGuardErr = IssueSessionControlCookie(c, testSessionControlProof, expiresAt)
		c.Status(http.StatusNoContent)
	})
	noGuardRec := httptest.NewRecorder()
	noGuardRouter.ServeHTTP(noGuardRec, admittedSessionControlRequestFor(http.MethodPost, "/issue"))
	if noGuardErr == nil || len(noGuardRec.Header().Values("Set-Cookie")) != 0 {
		t.Fatalf("unguarded issue err=%v cookies=%q, want error and no cookie", noGuardErr, noGuardRec.Header().Values("Set-Cookie"))
	}

	var committedErr error
	committedRouter := gin.New()
	committedRouter.POST("/issue", admission, func(c *gin.Context) {
		c.Writer.WriteHeaderNow()
		committedErr = IssueSessionControlCookie(c, testSessionControlProof, expiresAt)
	})
	committedRec := httptest.NewRecorder()
	ProtectSessionCredentialResponses(committedRouter).ServeHTTP(committedRec, admittedSessionControlRequestFor(http.MethodPost, "/issue"))
	if committedErr == nil || len(committedRec.Header().Values("Set-Cookie")) != 0 {
		t.Fatalf("committed issue err=%v cookies=%q, want error and no cookie", committedErr, committedRec.Header().Values("Set-Cookie"))
	}
}

func TestIssueSessionControlCookieRequiresAdmittedRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	var issueErr error
	router := gin.New()
	router.POST("/issue", func(c *gin.Context) {
		issueErr = IssueSessionControlCookie(c, testSessionControlProof, time.Now().Add(time.Hour))
		c.Status(http.StatusNoContent)
	})
	rec := httptest.NewRecorder()
	ProtectSessionCredentialResponses(router).ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/issue", nil))
	if issueErr != errSessionControlNotAdmitted {
		t.Fatalf("IssueSessionControlCookie err = %v, want %v", issueErr, errSessionControlNotAdmitted)
	}
	if got := rec.Header().Values("Set-Cookie"); len(got) != 0 {
		t.Fatalf("unadmitted request issued cookies: %q", got)
	}
}

func TestProtectedSessionControlCookieIsSuppressedForSwitchingProtocols(t *testing.T) {
	gin.SetMode(gin.TestMode)
	admission, err := NewSessionControlAdmission("https://app.example")
	if err != nil {
		t.Fatalf("NewSessionControlAdmission failed: %v", err)
	}
	var issueErr error
	router := gin.New()
	router.POST("/switch", admission, func(c *gin.Context) {
		issueErr = IssueSessionControlCookie(c, testSessionControlProof, time.Now().Add(time.Hour))
		c.Status(http.StatusSwitchingProtocols)
	})
	rec := httptest.NewRecorder()
	ProtectSessionCredentialResponses(router).ServeHTTP(rec, admittedSessionControlRequestFor(http.MethodPost, "/switch"))
	if issueErr != nil {
		t.Fatalf("IssueSessionControlCookie failed before switch: %v", issueErr)
	}
	if got := rec.Header().Values("Set-Cookie"); len(got) != 0 {
		t.Fatalf("switching-protocol response exposed cookie: %q", got)
	}
	if got := rec.Header().Get(headerCacheControl); got != cacheControlNoStore {
		t.Fatalf("switching-protocol %s = %q, want conservative %q", headerCacheControl, got, cacheControlNoStore)
	}
}

func TestPairedSessionCredentialsRaceResponseCommitAtomically(t *testing.T) {
	cookie, err := newSessionControlCookie(testSessionControlProof, time.Now().Add(time.Hour), time.Now())
	if err != nil {
		t.Fatalf("newSessionControlCookie failed: %v", err)
	}

	for i := 0; i < 1000; i++ {
		header := make(http.Header)
		state := &refreshedTokenResponseState{header: header}
		ctx := context.WithValue(context.Background(), refreshedTokenResponseContextKey{}, state)
		start := make(chan struct{})
		var wg sync.WaitGroup
		wg.Add(2)
		go func() {
			defer wg.Done()
			<-start
			_ = setSessionCredentialResponse(ctx, "paired-bearer", cookie)
		}()
		go func() {
			defer wg.Done()
			<-start
			state.commitHeaders()
		}()
		close(start)
		wg.Wait()

		hasBearer := header.Get(HeaderRefreshedToken) == "paired-bearer"
		hasCookie := false
		for _, value := range header.Values("Set-Cookie") {
			parsed, parseErr := http.ParseSetCookie(value)
			if parseErr == nil && parsed.Name == SessionControlCookieName && parsed.Value == testSessionControlProof {
				hasCookie = true
			}
		}
		if hasBearer != hasCookie {
			t.Fatalf("iteration %d delivered partial pair: bearer=%v cookie=%v headers=%q", i, hasBearer, hasCookie, header)
		}
		if hasBearer && header.Get(headerCacheControl) != cacheControlNoStore {
			t.Fatalf("iteration %d paired response %s=%q, want %q", i, headerCacheControl, header.Get(headerCacheControl), cacheControlNoStore)
		}
	}
}

func TestIssueSessionControlCookieRejectsMalformedProofOrExpiry(t *testing.T) {
	for _, proof := range []string{
		"",
		"short",
		strings.Repeat("x", maxSessionControlProofBytes+1),
		strings.Repeat("x", minSessionControlProofBytes) + "\n",
		strings.Repeat("x", minSessionControlProofBytes) + ";",
		strings.Repeat("x", minSessionControlProofBytes) + " embedded-space",
		strings.Repeat("x", minSessionControlProofBytes) + ",",
	} {
		if _, err := newSessionControlCookie(proof, time.Now().Add(time.Hour), time.Now()); err == nil {
			t.Fatalf("newSessionControlCookie(%q) succeeded, want error", proof)
		}
	}
	for _, expiresAt := range []time.Time{{}, time.Now().Add(-time.Second)} {
		if _, err := newSessionControlCookie(testSessionControlProof, expiresAt, time.Now()); err == nil {
			t.Fatalf("newSessionControlCookie expiry %v succeeded, want error", expiresAt)
		}
	}
}

func serveSessionControlRequest(t *testing.T, req *http.Request) (string, bool) {
	proof, ok, _ := serveSessionControlRequestState(t, req)
	return proof, ok
}

func serveSessionControlRequestState(t *testing.T, req *http.Request) (string, bool, bool) {
	proof, ok, admitted, _ := serveSessionControlRequestControlState(t, req)
	return proof, ok, admitted
}

func serveSessionControlRequestControlState(t *testing.T, req *http.Request) (string, bool, bool, bool) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	admission, err := NewSessionControlAdmission("https://app.example")
	if err != nil {
		t.Fatalf("NewSessionControlAdmission failed: %v", err)
	}
	var (
		proof     string
		ok        bool
		admitted  bool
		attempted bool
	)
	router := gin.New()
	router.GET("/control", admission, func(c *gin.Context) {
		proof, ok = SessionControlProof(c.Request.Context())
		admitted = SessionControlRequestAdmitted(c.Request.Context())
		attempted = sessionControlRequestAttempted(c.Request.Context())
		c.Status(http.StatusNoContent)
	})
	router.POST("/control", admission, func(c *gin.Context) {
		proof, ok = SessionControlProof(c.Request.Context())
		admitted = SessionControlRequestAdmitted(c.Request.Context())
		attempted = sessionControlRequestAttempted(c.Request.Context())
		c.Status(http.StatusNoContent)
	})
	router.ServeHTTP(httptest.NewRecorder(), req)
	return proof, ok, admitted, attempted
}

func admittedSessionControlRequest(method string) *http.Request {
	return admittedSessionControlRequestFor(method, "/control")
}

func admittedSessionControlRequestFor(method, target string) *http.Request {
	req := httptest.NewRequest(method, target, nil)
	req.Header.Set(HeaderSessionControlIntent, SessionControlIntentValue)
	req.Header.Set(headerSecFetchSite, secFetchSiteSameOrigin)
	req.Header.Set(headerOrigin, "https://app.example")
	return req
}

func assertSessionControlCookie(t *testing.T, cookie *http.Cookie, value string) {
	t.Helper()
	if cookie.Name != SessionControlCookieName || cookie.Value != value || cookie.Path != "/" ||
		!cookie.Secure || !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode || cookie.Domain != "" {
		t.Fatalf("cookie = %#v, want host-only Secure HttpOnly SameSite=Strict Path=/ value=%q", cookie, value)
	}
}
