package middleware

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

	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
)

const testProof = "PtEMSGDgtHRIeSlBSPCzUqjEDoHVFGiJFEbmqoXCPKk"

func headerWith(values ...string) http.Header {
	header := http.Header{}
	for i := 0; i+1 < len(values); i += 2 {
		header.Add(values[i], values[i+1])
	}
	return header
}

func proofCookie(value string) string {
	return SessionControlCookieName + "=" + value
}

// allPolicies is every declared value, so a newly added policy shows up in the
// exhaustive tests below instead of being silently unexercised.
func allPolicies() []dialect.CredentialPolicy {
	return []dialect.CredentialPolicy{
		dialect.CredentialProgrammaticOnly,
		dialect.CredentialBrowserOrProgrammatic,
		dialect.CredentialBrowserOnly,
		dialect.CredentialBrowserEstablishment,
		dialect.CredentialBrowserTermination,
	}
}

// TestSelectorRejectsEveryPolicyWhenTheAuthorizationHeaderIsAmbiguous covers the
// rule that ambiguity is resolved before any lane is taken. It holds even for the
// browser policies that reject the header outright, so the reason is about the
// ambiguity rather than about the policy.
func TestSelectorRejectsEveryPolicyWhenTheAuthorizationHeaderIsAmbiguous(t *testing.T) {
	header := headerWith("Authorization", "Bearer one", "Authorization", "Bearer two")

	for _, policy := range allPolicies() {
		got := SelectRequestCredentialLane(header, policy, CredentialSelectorOptions{})
		if !got.Rejected() {
			t.Fatalf("policy %s: lane = %s, want rejected", policy, got.Lane)
		}
		if !strings.Contains(got.Reason, "more than one Authorization header") {
			t.Fatalf("policy %s: reason = %q, want the ambiguity named", policy, got.Reason)
		}
	}
}

// TestSelectorRejectsAnUnknownPolicy pins the fail-closed default for a value
// outside the declared set.
func TestSelectorRejectsAnUnknownPolicy(t *testing.T) {
	got := SelectRequestCredentialLane(http.Header{}, dialect.CredentialPolicy(99), CredentialSelectorOptions{})
	if !got.Rejected() {
		t.Fatalf("lane = %s, want rejected", got.Lane)
	}
	if !strings.Contains(got.Reason, "unknown credential policy") {
		t.Fatalf("reason = %q, want it to name the unknown policy", got.Reason)
	}
}

// TestSelectorRejectsAuthorizationOnEveryBrowserOnlyPolicy pins the design rule
// that these policies reject the header *before* authentication, so presenting a
// Bearer can never reach a credential-validating path on them.
func TestSelectorRejectsAuthorizationOnEveryBrowserOnlyPolicy(t *testing.T) {
	for _, policy := range []dialect.CredentialPolicy{
		dialect.CredentialBrowserOnly,
		dialect.CredentialBrowserEstablishment,
		dialect.CredentialBrowserTermination,
	} {
		header := headerWith("Authorization", "Bearer x", "Cookie", proofCookie(testProof))
		got := SelectRequestCredentialLane(header, policy, CredentialSelectorOptions{})
		if !got.Rejected() {
			t.Fatalf("policy %s: lane = %s, want the Authorization header rejected", policy, got.Lane)
		}
		if !strings.Contains(got.Reason, "Authorization header") {
			t.Fatalf("policy %s: reason = %q, want it to name the header", policy, got.Reason)
		}
	}
}

// TestSelectorFailsClosedOnTheProofAndBearerPair is the final-state rule: a
// request presenting both credentials is rejected rather than letting the caller
// choose a lane by presenting two.
func TestSelectorFailsClosedOnTheProofAndBearerPair(t *testing.T) {
	header := headerWith("Authorization", "Bearer x", "Cookie", proofCookie(testProof))

	got := SelectRequestCredentialLane(header, dialect.CredentialBrowserOrProgrammatic, CredentialSelectorOptions{})
	if !got.Rejected() {
		t.Fatalf("lane = %s, want the pair rejected in the final state", got.Lane)
	}
	if !strings.Contains(got.Reason, "both") {
		t.Fatalf("reason = %q, want it to name the pair", got.Reason)
	}
}

// TestSelectorAllowsTheProofAndBearerPairOnlyInTheCompatibilityWindow pins that
// the allowance is explicit and that the Bearer stays authoritative while it is
// open, so opening the window does not change which credential decides identity.
func TestSelectorAllowsTheProofAndBearerPairOnlyInTheCompatibilityWindow(t *testing.T) {
	header := headerWith("Authorization", "Bearer x", "Cookie", proofCookie(testProof))

	got := SelectRequestCredentialLane(header, dialect.CredentialBrowserOrProgrammatic, CredentialSelectorOptions{
		AllowBearerAlongsideBrowserProof: true,
	})
	if got.Lane != CredentialLaneProgrammatic {
		t.Fatalf("lane = %s, want the programmatic lane while the Bearer remains authoritative", got.Lane)
	}
}

// TestSelectorRejectsABrowserCookieOnAProgrammaticOnlyRouteByDefault pins that the
// enforced state is the zero value, so forgetting to configure the selector does
// not silently keep the migration allowance.
func TestSelectorRejectsABrowserCookieOnAProgrammaticOnlyRouteByDefault(t *testing.T) {
	header := headerWith("Authorization", "Bearer x", "Cookie", proofCookie(testProof))

	got := SelectRequestCredentialLane(header, dialect.CredentialProgrammaticOnly, CredentialSelectorOptions{})
	if !got.Rejected() {
		t.Fatalf("lane = %s, want a programmatic-only route to reject the browser cookie", got.Lane)
	}

	allowed := SelectRequestCredentialLane(header, dialect.CredentialProgrammaticOnly, CredentialSelectorOptions{
		AllowBrowserProofOnProgrammaticOnlyRoutes: true,
	})
	if allowed.Lane != CredentialLaneProgrammatic {
		t.Fatalf("lane = %s, want the programmatic lane while the allowance is open", allowed.Lane)
	}
}

// TestSelectorTakesTheBrowserLaneForAValidProofAlone covers the target state of
// the migration: a cookie and no Authorization header.
func TestSelectorTakesTheBrowserLaneForAValidProofAlone(t *testing.T) {
	header := headerWith("Cookie", proofCookie(testProof))

	for _, policy := range []dialect.CredentialPolicy{
		dialect.CredentialBrowserOrProgrammatic,
		dialect.CredentialBrowserOnly,
		dialect.CredentialBrowserTermination,
	} {
		got := SelectRequestCredentialLane(header, policy, CredentialSelectorOptions{})
		if got.Lane != CredentialLaneBrowserSession {
			t.Fatalf("policy %s: lane = %s, want the browser lane (%s)", policy, got.Lane, got.Reason)
		}
	}
}

// TestSelectorRejectsDuplicateProofCookiesWhereAPrincipalIsRequired pins "exactly
// one structurally valid proof cookie" for the policies that need a principal.
func TestSelectorRejectsDuplicateProofCookiesWhereAPrincipalIsRequired(t *testing.T) {
	header := headerWith("Cookie", proofCookie(testProof), "Cookie", proofCookie(testProof))

	for _, policy := range []dialect.CredentialPolicy{
		dialect.CredentialBrowserOrProgrammatic,
		dialect.CredentialBrowserOnly,
	} {
		got := SelectRequestCredentialLane(header, policy, CredentialSelectorOptions{})
		if !got.Rejected() {
			t.Fatalf("policy %s: lane = %s, want duplicate cookies rejected", policy, got.Lane)
		}
		if !strings.Contains(got.Reason, "more than one session-control cookie") {
			t.Fatalf("policy %s: reason = %q, want it to name the duplicate", policy, got.Reason)
		}
	}
}

// TestSelectorRejectsAMalformedProofWhereAPrincipalIsRequired keeps a structurally
// invalid cookie from reaching session resolution.
func TestSelectorRejectsAMalformedProofWhereAPrincipalIsRequired(t *testing.T) {
	header := headerWith("Cookie", proofCookie("short"))

	for _, policy := range []dialect.CredentialPolicy{
		dialect.CredentialBrowserOrProgrammatic,
		dialect.CredentialBrowserOnly,
	} {
		got := SelectRequestCredentialLane(header, policy, CredentialSelectorOptions{})
		if !got.Rejected() {
			t.Fatalf("policy %s: lane = %s, want a malformed proof rejected", policy, got.Lane)
		}
	}
}

// TestTerminationAdmitsAnUnusableProofWithoutAPrincipal is the logout rule, and
// the one place a malformed or duplicated proof must NOT be rejected: rejecting
// would make logout an oracle for whether a proof was well formed, and would
// leave a stale browser unable to clear its cookie.
func TestTerminationAdmitsAnUnusableProofWithoutAPrincipal(t *testing.T) {
	cases := map[string]http.Header{
		"no cookie at all":  http.Header{},
		"malformed proof":   headerWith("Cookie", proofCookie("short")),
		"duplicate cookies": headerWith("Cookie", proofCookie(testProof), "Cookie", proofCookie(testProof)),
	}

	for name, header := range cases {
		t.Run(name, func(t *testing.T) {
			got := SelectRequestCredentialLane(header, dialect.CredentialBrowserTermination, CredentialSelectorOptions{})
			if got.Lane != CredentialLaneFirstPartyUnauthenticated {
				t.Fatalf("lane = %s, want first-party admission without a principal (%s)", got.Lane, got.Reason)
			}
		})
	}
}

// TestEstablishmentNeverReadsAStaleCookieAsAPrincipal pins that establishment
// admits without a principal whether or not a prior cookie is attached, so the
// caller cannot choose the credential it ends up with.
func TestEstablishmentNeverReadsAStaleCookieAsAPrincipal(t *testing.T) {
	for name, header := range map[string]http.Header{
		"no cookie":          http.Header{},
		"valid prior cookie": headerWith("Cookie", proofCookie(testProof)),
		"malformed prior":    headerWith("Cookie", proofCookie("short")),
		"duplicate prior":    headerWith("Cookie", proofCookie(testProof), "Cookie", proofCookie(testProof)),
	} {
		t.Run(name, func(t *testing.T) {
			got := SelectRequestCredentialLane(header, dialect.CredentialBrowserEstablishment, CredentialSelectorOptions{})
			if got.Lane != CredentialLaneFirstPartyUnauthenticated {
				t.Fatalf("lane = %s, want first-party admission without a principal (%s)", got.Lane, got.Reason)
			}
		})
	}
}

// allowedSelectorReasons is every rejection message the selector may produce. A
// reason built by interpolating request data cannot appear in this set, so the
// matrix test below fails the moment one is introduced.
var allowedSelectorReasons = map[string]struct{}{
	"route declares an unknown credential policy":                                       {},
	"route declares an unhandled credential policy":                                     {},
	"request carries more than one Authorization header":                                {},
	"request carries more than one session-control cookie":                              {},
	"programmatic-only route rejects a first-party browser session cookie":              {},
	"programmatic-only route requires an Authorization credential":                      {},
	"request carries both a browser session cookie and an Authorization credential":     {},
	"session-control cookie is not structurally valid":                                  {},
	"route requires either a browser session cookie or an Authorization credential":     {},
	"browser-only route rejects the Authorization header before authentication":         {},
	"browser-only route requires exactly one structurally valid session-control cookie": {},
	"session establishment rejects the Authorization header":                            {},
	"session termination rejects the Authorization header":                              {},
}

// TestSelectorNeverAdmitsMoreThanOneLane is the structural invariant behind the
// whole selector: for every policy and every credential combination it returns
// one lane, and a rejection always carries a reason while an admission never
// does. Enumerating the combinations is what makes a future policy or option
// unable to introduce a silent third state.
func TestSelectorNeverAdmitsMoreThanOneLane(t *testing.T) {
	headers := map[string]http.Header{
		"nothing":          http.Header{},
		"bearer":           headerWith("Authorization", "Bearer x"),
		"proof":            headerWith("Cookie", proofCookie(testProof)),
		"pair":             headerWith("Authorization", "Bearer x", "Cookie", proofCookie(testProof)),
		"malformed proof":  headerWith("Cookie", proofCookie("short")),
		"duplicate proof":  headerWith("Cookie", proofCookie(testProof), "Cookie", proofCookie(testProof)),
		"duplicate bearer": headerWith("Authorization", "Bearer x", "Authorization", "Bearer y"),
		"unrelated cookie": headerWith("Cookie", "other=1"),
	}
	options := map[string]CredentialSelectorOptions{
		"final state":      {},
		"pair allowed":     {AllowBearerAlongsideBrowserProof: true},
		"cookie tolerated": {AllowBrowserProofOnProgrammaticOnlyRoutes: true},
		"both allowances":  {AllowBearerAlongsideBrowserProof: true, AllowBrowserProofOnProgrammaticOnlyRoutes: true},
	}

	for _, policy := range allPolicies() {
		for headerName, header := range headers {
			for optionName, opts := range options {
				got := SelectRequestCredentialLane(header, policy, opts)
				switch got.Lane {
				case CredentialLaneRejected:
					if got.Reason == "" {
						t.Fatalf("%s/%s/%s: rejection carries no reason", policy, headerName, optionName)
					}
				case CredentialLaneProgrammatic, CredentialLaneBrowserSession, CredentialLaneFirstPartyUnauthenticated:
					if got.Reason != "" {
						t.Fatalf("%s/%s/%s: admitted lane %s also carries reason %q",
							policy, headerName, optionName, got.Lane, got.Reason)
					}
				default:
					t.Fatalf("%s/%s/%s: lane %d is outside the declared set", policy, headerName, optionName, got.Lane)
				}

				// A rejection reason is reachable by an unauthenticated caller, so
				// it must be one of a fixed set of constant messages. Checking
				// membership rather than searching for known credential values is
				// what makes *any* interpolation of request data fail here,
				// including a malformed credential a substring check would miss.
				if got.Reason != "" {
					if _, allowed := allowedSelectorReasons[got.Reason]; !allowed {
						t.Fatalf("%s/%s/%s: reason %q is not a constant selector message; reasons must never interpolate request data",
							policy, headerName, optionName, got.Reason)
					}
				}
			}
		}
	}
}

// TestSelectorIgnoresUnrelatedCookies keeps an unrelated cookie from being read as
// browser credential presence, which would otherwise reject programmatic-only
// requests from any browser that holds any cookie at all.
func TestSelectorIgnoresUnrelatedCookies(t *testing.T) {
	header := headerWith("Authorization", "Bearer x", "Cookie", "other=1; another=2")

	got := SelectRequestCredentialLane(header, dialect.CredentialProgrammaticOnly, CredentialSelectorOptions{})
	if got.Lane != CredentialLaneProgrammatic {
		t.Fatalf("lane = %s, want unrelated cookies ignored (%s)", got.Lane, got.Reason)
	}
}

// TestLaneStringsAreStableAndDistinct keeps the lane names usable as metric labels
// for the canary step without silently colliding.
func TestLaneStringsAreStableAndDistinct(t *testing.T) {
	seen := map[string]struct{}{}
	for _, lane := range []CredentialLane{
		CredentialLaneRejected,
		CredentialLaneProgrammatic,
		CredentialLaneBrowserSession,
		CredentialLaneFirstPartyUnauthenticated,
	} {
		name := lane.String()
		if name == "" || name == "unknown" {
			t.Fatalf("lane %d has no stable name", lane)
		}
		if _, duplicate := seen[name]; duplicate {
			t.Fatalf("lane name %q is used twice", name)
		}
		seen[name] = struct{}{}
	}
	if got := CredentialLane(99).String(); got != "unknown" {
		t.Fatalf("undeclared lane name = %q, want unknown", got)
	}
}

// TestSelectorRejectsAQuotedProofOnEveryBrowserPolicy is the parser-mismatch
// guard. Go's cookie parser strips surrounding quotes and records Quoted, so a
// quoted value can look structurally valid after parsing while the bytes on the
// wire were not what any peer agreed on. Session-control admission has always
// rejected it; the selector must reach the same conclusion, or a request can be
// classified into the browser lane by one component and treated as carrying no
// credential by the other.
func TestSelectorRejectsAQuotedProofOnEveryBrowserPolicy(t *testing.T) {
	header := headerWith("Cookie", proofCookie(`"`+testProof+`"`))

	for _, policy := range []dialect.CredentialPolicy{
		dialect.CredentialBrowserOrProgrammatic,
		dialect.CredentialBrowserOnly,
	} {
		got := SelectRequestCredentialLane(header, policy, CredentialSelectorOptions{})
		if got.Lane == CredentialLaneBrowserSession {
			t.Fatalf("policy %s: a quoted proof must not be classified as a browser session", policy)
		}
		if !got.Rejected() {
			t.Fatalf("policy %s: lane = %s, want rejected", policy, got.Lane)
		}
	}

	// Termination still admits without a principal: an unusable proof must not
	// become a logout oracle, quoted or otherwise.
	termination := SelectRequestCredentialLane(header, dialect.CredentialBrowserTermination, CredentialSelectorOptions{})
	if termination.Lane != CredentialLaneFirstPartyUnauthenticated {
		t.Fatalf("termination lane = %s, want first-party admission without a principal", termination.Lane)
	}
}

// TestUsableSessionControlProofRejectsEveryLookalike pins the one parser both the
// admission and the selector use. Each case is a value that survives some weaker
// check but must not be treated as a credential.
func TestUsableSessionControlProofRejectsEveryLookalike(t *testing.T) {
	usable := map[string]http.Header{
		"exactly one valid proof": headerWith("Cookie", proofCookie(testProof)),
	}
	unusable := map[string]http.Header{
		"no cookie":                {},
		"quoted value":             headerWith("Cookie", proofCookie(`"`+testProof+`"`)),
		"duplicate in two headers": headerWith("Cookie", proofCookie(testProof), "Cookie", proofCookie(testProof)),
		"duplicate in one header":  headerWith("Cookie", proofCookie(testProof)+"; "+proofCookie(testProof)),
		"too short":                headerWith("Cookie", proofCookie("short")),
		"embedded space":           headerWith("Cookie", proofCookie(testProof[:10]+" "+testProof[10:])),
		"only an unrelated cookie": headerWith("Cookie", "other=1"),
	}

	for name, header := range usable {
		t.Run("usable/"+name, func(t *testing.T) {
			proof, ok := usableSessionControlProof(header)
			if !ok {
				t.Fatal("want the proof accepted")
			}
			if proof != testProof {
				t.Fatalf("proof = %q, want the cookie value", proof)
			}
		})
	}
	for name, header := range unusable {
		t.Run("unusable/"+name, func(t *testing.T) {
			proof, ok := usableSessionControlProof(header)
			if ok {
				t.Fatalf("want the value rejected, got %q", proof)
			}
			if proof != "" {
				t.Fatalf("a rejected parse must return no proof, got %q", proof)
			}
		})
	}
}

// TestSelectorBrowserLaneMatchesTheSharedParser is the anti-drift assertion: for
// every cookie shape, the selector admits the browser lane exactly when the shared
// parser yields a proof. A selector that re-derived its own rule would break this
// as soon as the two disagreed on one value.
func TestSelectorBrowserLaneMatchesTheSharedParser(t *testing.T) {
	for name, header := range map[string]http.Header{
		"valid":              headerWith("Cookie", proofCookie(testProof)),
		"quoted":             headerWith("Cookie", proofCookie(`"`+testProof+`"`)),
		"duplicate":          headerWith("Cookie", proofCookie(testProof), "Cookie", proofCookie(testProof)),
		"malformed":          headerWith("Cookie", proofCookie("short")),
		"absent":             {},
		"unrelated only":     headerWith("Cookie", "other=1"),
		"trailing semicolon": headerWith("Cookie", proofCookie(testProof)+";"),
	} {
		t.Run(name, func(t *testing.T) {
			_, parserAccepts := usableSessionControlProof(header)
			selection := SelectRequestCredentialLane(header, dialect.CredentialBrowserOnly, CredentialSelectorOptions{})
			selectorAccepts := selection.Lane == CredentialLaneBrowserSession
			if parserAccepts != selectorAccepts {
				t.Fatalf("shared parser accepts = %v but selector browser lane = %v (%s); the authentication boundary must have one parser",
					parserAccepts, selectorAccepts, selection.Reason)
			}
		})
	}
}
