package middleware

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

	"github.com/gin-gonic/gin"

	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

const browserLaneTestProof = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

// browserLaneHarness is one admission composition plus the router that exercises it. The
// admitter and snapshot provider are the same stubs the bearer tests use, deliberately:
// the claim being tested is that the browser lane reaches the *same* admission and the
// same Permission State resolution, so it must not be measured against a different one.
type browserLaneHarness struct {
	admitter  *authorizingAdmitterStub
	provider  *authorizingSnapshotProviderStub
	writer    *authorizingSessionWriterStub
	handler   http.Handler
	reachedOK *bool
}

func newBrowserLaneHarness(t *testing.T, laneEnabled bool, admission hostruntime.AuthorizingSessionAdmission) *browserLaneHarness {
	t.Helper()
	gin.SetMode(gin.TestMode)

	snapshot, _, _ := browserLaneSnapshot(t)

	cache, err := NewPermissionStateCache(DefaultPermissionStateCacheTTL)
	if err != nil {
		t.Fatal(err)
	}
	admitter := &authorizingAdmitterStub{admission: admission}
	provider := &authorizingSnapshotProviderStub{snapshot: snapshot}
	writer := &authorizingSessionWriterStub{}
	middleware, err := NewAuthorizingSessionMiddleware(AuthorizingSessionMiddlewareOptions{
		Admitter:           admitter,
		SnapshotProvider:   provider,
		SnapshotCache:      cache,
		SessionWriter:      writer,
		PermissionContext:  &authorizingPermissionContextStub{},
		BrowserSessionLane: laneEnabled,
	})
	if err != nil {
		t.Fatal(err)
	}

	admission2, err := NewSessionControlAdmission("https://app.example.com")
	if err != nil {
		t.Fatal(err)
	}

	reached := false
	router := gin.New()
	router.Use(admission2)
	router.GET("/", middleware.RequireAuth(), func(c *gin.Context) {
		// Asserted inside the handler: the whole point of this lane is that a cookie-only
		// request arrives with the same authorization state a bearer request would.
		state, ok := hostruntime.AuthorizingPermissionStateFromContext(c.Request.Context())
		if !ok || state.Snapshot.Version().Revision() != 4 {
			t.Errorf("request Permission State = %#v, %t; want the resolved snapshot", state, ok)
		}
		reached = true
		c.Status(http.StatusNoContent)
	})

	return &browserLaneHarness{
		admitter: admitter,
		provider: provider,
		writer:   writer,
		// Wrapped the way the host wraps its router. The credential-response boundary owns
		// Set-Cookie, and a renewal without it fails the request rather than serving a
		// session whose cookie should have been extended — so a harness without it would
		// answer 503 and the renewal assertion would be measuring the missing boundary
		// instead of the lane.
		handler:   ProtectSessionCredentialResponses(router),
		reachedOK: &reached,
	}
}

// browserLaneSnapshot builds the same fixture the bearer tests use, so "the browser lane
// resolves the same Permission State" is asserted against the identical snapshot rather
// than a lookalike.
func browserLaneSnapshot(t *testing.T) (permissionstatev1.EffectiveSnapshot, permissionstatev1.Subject, permissionstatev1.Version) {
	t.Helper()
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 4)
	snapshot, err := permissionstatev1.NewEffectiveSnapshot(
		version,
		[]string{"ApiEnabled"},
		[]string{"ViewRevenue"},
	)
	if err != nil {
		t.Fatal(err)
	}
	return snapshot, subject, version
}

type browserLaneRequest struct {
	authHeader string
	cookie     string
	intent     bool
	firstParty bool
}

func (h *browserLaneHarness) run(t *testing.T, spec browserLaneRequest) *httptest.ResponseRecorder {
	t.Helper()
	return h.serve(t, spec, spec.authHeader != "", spec.authHeader)
}

// runWithExplicitAuthorization sets the header even when the value is empty, which is the
// case that distinguishes "present and unusable" from "absent". run() cannot express it,
// since it treats an empty string as "do not set".
func (h *browserLaneHarness) runWithExplicitAuthorization(t *testing.T, authHeader string, spec browserLaneRequest) *httptest.ResponseRecorder {
	t.Helper()
	return h.serve(t, spec, true, authHeader)
}

func (h *browserLaneHarness) serve(t *testing.T, spec browserLaneRequest, setAuthorization bool, authHeader string) *httptest.ResponseRecorder {
	t.Helper()
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	if setAuthorization {
		request.Header.Set(headerAuthorization, authHeader)
	}
	if spec.cookie != "" {
		request.Header.Add("Cookie", SessionControlCookieName+"="+spec.cookie)
	}
	if spec.intent {
		request.Header.Set(HeaderSessionControlIntent, SessionControlIntentValue)
	}
	if spec.firstParty {
		request.Header.Set("Sec-Fetch-Site", "same-origin")
		request.Header.Set("Origin", "https://app.example.com")
	} else {
		request.Header.Set("Sec-Fetch-Site", "cross-site")
		request.Header.Set("Origin", "https://evil.example.com")
	}

	response := httptest.NewRecorder()
	h.handler.ServeHTTP(response, request)
	return response
}

func browserLaneAdmission(t *testing.T) hostruntime.AuthorizingSessionAdmission {
	t.Helper()
	_, subject, version := browserLaneSnapshot(t)
	return testAuthorizingAdmission(subject, version)
}

// TestBrowserLaneAdmitsACookieOnlyRequestThroughTheSamePath is the capability. A
// first-party request with a usable proof and no Authorization header at all is admitted,
// and it arrives with the resolved Permission State — not through a path that skips it.
func TestBrowserLaneAdmitsACookieOnlyRequestThroughTheSamePath(t *testing.T) {
	h := newBrowserLaneHarness(t, true, browserLaneAdmission(t))

	response := h.run(t, browserLaneRequest{cookie: browserLaneTestProof, intent: true, firstParty: true})

	if response.Code != http.StatusNoContent {
		t.Fatalf("status = %d, want 204 (body %q)", response.Code, response.Body.String())
	}
	if !*h.reachedOK {
		t.Fatal("the handler never ran, so nothing about the admitted state was checked")
	}
	if h.admitter.calls != 1 {
		t.Fatalf("admitter calls = %d, want 1", h.admitter.calls)
	}
	if h.admitter.request.Mode != hostruntime.AuthorizingSessionBrowser {
		t.Fatalf("mode = %d, want the browser mode", h.admitter.request.Mode)
	}
	if h.admitter.request.BearerToken != "" {
		t.Fatalf("bearer = %q, want empty: this lane exists because there is no bearer", h.admitter.request.BearerToken)
	}
	if h.admitter.request.ControlProof != browserLaneTestProof {
		t.Fatalf("control proof = %q, want the presented proof", h.admitter.request.ControlProof)
	}
	if h.provider.calls != 1 {
		t.Fatalf("snapshot provider calls = %d, want 1: the permission floor must be resolved for this lane too", h.provider.calls)
	}
}

// TestBrowserLaneIsInertWhenDisabled is the compatibility guarantee, and it is the reason
// the option defaults to off. A host that has not opted in must see the same rejection it
// sees today, and the admitter must not be consulted at all.
func TestBrowserLaneIsInertWhenDisabled(t *testing.T) {
	h := newBrowserLaneHarness(t, false, browserLaneAdmission(t))

	response := h.run(t, browserLaneRequest{cookie: browserLaneTestProof, intent: true, firstParty: true})

	if response.Code != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401 when the lane is off", response.Code)
	}
	if h.admitter.calls != 0 {
		t.Fatalf("admitter calls = %d, want 0: a disabled lane must not reach admission", h.admitter.calls)
	}
}

// TestBrowserLaneRefusesWithoutFirstPartyEvidence covers the CSRF shape. A cookie is
// ambient — the browser attaches it to requests the user never initiated — so the proof
// alone is not a credential. Both halves of the evidence are required, and the no-intent
// case is the one the shared attempted/admitted check cannot catch, because nothing
// declared intent for it to check.
func TestBrowserLaneRefusesWithoutFirstPartyEvidence(t *testing.T) {
	cases := map[string]browserLaneRequest{
		"no intent header": {cookie: browserLaneTestProof, intent: false, firstParty: true},
		"cross-site":       {cookie: browserLaneTestProof, intent: true, firstParty: false},
		"neither":          {cookie: browserLaneTestProof, intent: false, firstParty: false},
	}
	for name, spec := range cases {
		t.Run(name, func(t *testing.T) {
			h := newBrowserLaneHarness(t, true, browserLaneAdmission(t))

			if response := h.run(t, spec); response.Code != http.StatusUnauthorized {
				t.Fatalf("status = %d, want 401: an ambient cookie without declared first-party intent is a cross-site request", response.Code)
			}
			if h.admitter.calls != 0 {
				t.Fatalf("admitter calls = %d, want 0", h.admitter.calls)
			}
		})
	}
}

// TestBrowserLaneRefusesAnUnusableProof keeps the lane from admitting on a proof the
// cookie parser rejects. A duplicated, quoted or malformed cookie is not a credential, and
// falling through to admission with an empty or ambiguous value is how a lane authenticates
// on nothing.
func TestBrowserLaneRefusesAnUnusableProof(t *testing.T) {
	for name, cookie := range map[string]string{
		"too short": "short",
		"empty":     "",
	} {
		t.Run(name, func(t *testing.T) {
			h := newBrowserLaneHarness(t, true, browserLaneAdmission(t))

			if response := h.run(t, browserLaneRequest{cookie: cookie, intent: true, firstParty: true}); response.Code != http.StatusUnauthorized {
				t.Fatalf("status = %d, want 401", response.Code)
			}
			if h.admitter.calls != 0 {
				t.Fatalf("admitter calls = %d, want 0", h.admitter.calls)
			}
		})
	}
}

// TestBrowserLaneNeverRescuesABrokenBearer is the rule that keeps the lanes separate. A
// malformed Authorization header is a rejected credential; if an ambient cookie could
// rescue it, a caller whose bearer was revoked or truncated would silently keep working
// through a different lane, and the two lanes' revocation semantics would diverge
// invisibly.
//
// The shapes are enumerated rather than sampled. The first version of this test asserted
// only "Bearer" — the malformed value that happened to come to mind — and review found the
// one that broke the rule: a present-but-empty header, which c.GetHeader reports
// identically to an absent one. Enumerating the ways a header can be present and unusable
// is what makes the rule tested rather than illustrated.
func TestBrowserLaneNeverRescuesABrokenBearer(t *testing.T) {
	for name, authHeader := range map[string]string{
		"scheme with no token":    "Bearer",
		"scheme with blank token": "Bearer ",
		"present but empty":       "",
		"whitespace only":         " ",
		"wrong scheme":            "Basic dXNlcjpwYXNz",
		"token with no scheme":    "sometoken",
		"lowercase scheme":        "bearer sometoken",
	} {
		t.Run(name, func(t *testing.T) {
			h := newBrowserLaneHarness(t, true, browserLaneAdmission(t))

			response := h.runWithExplicitAuthorization(t, authHeader, browserLaneRequest{
				cookie:     browserLaneTestProof,
				intent:     true,
				firstParty: true,
			})

			if response.Code != http.StatusUnauthorized {
				t.Fatalf("status = %d, want 401: a present-but-unusable Authorization header must not fall through to the cookie", response.Code)
			}
			if h.admitter.calls != 0 {
				t.Fatalf("admitter calls = %d, want 0", h.admitter.calls)
			}
		})
	}
}

// TestBrowserLaneReissuesItsCookieWithoutABearer is the renewal path. A browser admission
// that reports a new expiry must reissue the same proof cookie — and it has no bearer to
// accompany it, so it cannot go through the credential-*pair* response owner, which
// rejects an empty bearer. Review caught that: every browser renewal answered 503 instead
// of extending the session, so a session whose cookie should have been renewed would have
// expired while the user was active.
func TestBrowserLaneReissuesItsCookieWithoutABearer(t *testing.T) {
	admission := browserLaneAdmission(t)
	admission.ProofCookieExpiresAt = time.Now().Add(time.Hour)
	h := newBrowserLaneHarness(t, true, admission)

	response := h.run(t, browserLaneRequest{cookie: browserLaneTestProof, intent: true, firstParty: true})

	if response.Code != http.StatusNoContent {
		t.Fatalf("status = %d, want 204 (body %q)", response.Code, response.Body.String())
	}
	setCookie := response.Header().Get("Set-Cookie")
	if !strings.Contains(setCookie, SessionControlCookieName+"=") {
		t.Fatalf("Set-Cookie = %q, want the proof cookie reissued through its new expiry", setCookie)
	}
	if !strings.Contains(setCookie, "HttpOnly") {
		t.Fatalf("Set-Cookie = %q, want HttpOnly", setCookie)
	}
	// The renewal must not smuggle a credential into the response either.
	if header := response.Header().Get(HeaderRefreshedToken); header != "" {
		t.Fatalf("%s = %q, want none on the cookie-only lane", HeaderRefreshedToken, header)
	}
}

// TestBrowserLaneRejectsAnAdmissionThatMintsABearer is the structural guarantee. There is
// no credential to replace in this lane, so an admitter returning one is a contract
// violation — and delivering it would hand the page exactly the readable credential this
// lane exists to withhold. It fails the request rather than passing the token on.
func TestBrowserLaneRejectsAnAdmissionThatMintsABearer(t *testing.T) {
	admission := browserLaneAdmission(t)
	admission.RefreshedBearerToken = "a.replacement.bearer"
	h := newBrowserLaneHarness(t, true, admission)

	response := h.run(t, browserLaneRequest{cookie: browserLaneTestProof, intent: true, firstParty: true})

	if response.Code == http.StatusNoContent {
		t.Fatal("a bearer-minting admission was accepted on the cookie-only lane")
	}
	if *h.reachedOK {
		t.Fatal("the handler ran despite an invalid admission")
	}
	if body := response.Body.String(); body != "" {
		for _, forbidden := range []string{"a.replacement.bearer", "Bearer"} {
			if strings.Contains(body, forbidden) {
				t.Fatalf("response leaked the replacement credential: %s", body)
			}
		}
	}
	if header := response.Header().Get("Authorization"); header != "" {
		t.Fatalf("Authorization header = %q, want none", header)
	}
}
