package scim

import (
	"encoding/json"
	"testing"
)

func userBody(t *testing.T, m map[string]any) []byte {
	t.Helper()
	if _, ok := m["schemas"]; !ok {
		m["schemas"] = []any{SchemaUser}
	}
	b, err := json.Marshal(m)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	return b
}

func alwaysResolve(id string) ManagerResolver {
	return func(string) (string, bool, error) { return id, true, nil }
}
func neverResolve() ManagerResolver {
	return func(string) (string, bool, error) { return "", false, nil }
}

// patchOps wraps an Operations array literal in a well-formed PatchOp envelope
// (schema declaration required by ApplyPatch).
func patchOps(ops string) []byte {
	return []byte(`{"schemas":["` + SchemaPatchOp + `"],"Operations":` + ops + `}`)
}

// patchOpsJSON builds a PatchOp body from operations given as Go values, so
// paths containing quotes are escaped correctly.
func patchOpsJSON(t *testing.T, ops ...map[string]any) []byte {
	t.Helper()
	b, err := json.Marshal(map[string]any{"schemas": []string{SchemaPatchOp}, "Operations": ops})
	if err != nil {
		t.Fatalf("marshal patch: %v", err)
	}
	return b
}

func TestParseUser_RejectsNonUserAndEmpty(t *testing.T) {
	if _, err := ParseUser([]byte(`{"schemas":["urn:other"]}`)); err == nil {
		t.Fatal("expected rejection of non-User payload")
	}
	if _, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"]}`)); err == nil {
		t.Fatal("expected rejection when neither userName nor email present")
	}
	if _, err := ParseUser([]byte(`{not json`)); err == nil {
		t.Fatal("expected rejection of invalid JSON")
	}
}

func TestMapUser_CreateMapsFieldsNeverPassword(t *testing.T) {
	body := userBody(t, map[string]any{
		"userName": "wei.zhang@pingcap.com",
		"title":    "Engineer",
		"name":     map[string]any{"givenName": "Wei", "familyName": "Zhang"},
		"emails":   []any{map[string]any{"value": "wei.zhang@pingcap.com", "primary": true}},
		"phoneNumbers": []any{
			map[string]any{"type": "mobile", "value": "+86-1"},
			map[string]any{"type": "work", "value": "+86-2"},
		},
		SchemaEnterprise: map[string]any{"department": "Eng", "division": "R&D", "organization": "PingCAP"},
	})
	u, err := ParseUser(body)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	fm, err := u.MapUser(false, nil)
	if err != nil {
		t.Fatalf("map: %v", err)
	}
	want := map[string]string{
		FieldUsername: "wei.zhang@pingcap.com", FieldEmail: "wei.zhang@pingcap.com",
		FieldFirstName: "Wei", FieldLastName: "Zhang", FieldTitle: "Engineer",
		FieldMobilePhone: "+86-1", FieldPhone: "+86-2",
		FieldDepartment: "Eng",
	}
	for k, v := range want {
		if got, _ := fm.Fields[k].(string); got != v {
			t.Errorf("%s = %q, want %q", k, got, v)
		}
	}
	for _, ignored := range []string{"Division", "CompanyName"} {
		if got, present := fm.Fields[ignored]; present {
			t.Errorf("%s must not be mapped, got %v", ignored, got)
		}
	}
	if _, leaked := fm.Fields["Password"]; leaked {
		t.Error("Password must never be mapped (SSO-only)")
	}
}

func TestMapUser_ActiveParsing(t *testing.T) {
	// absent -> nil (preserve current)
	u, _ := ParseUser(userBody(t, map[string]any{"userName": "a@b.co"}))
	fm, err := u.MapUser(false, nil)
	if err != nil || fm.Active != nil {
		t.Fatalf("absent active should be nil, got %v err %v", fm.Active, err)
	}
	// explicit false
	u, _ = ParseUser(userBody(t, map[string]any{"userName": "a@b.co", "active": false}))
	fm, _ = u.MapUser(false, nil)
	if fm.Active == nil || *fm.Active {
		t.Fatalf("active=false should map to false")
	}
	// malformed -> 400
	u, _ = ParseUser(userBody(t, map[string]any{"userName": "a@b.co", "active": "garbage"}))
	if _, err := u.MapUser(false, nil); err == nil {
		t.Fatal("malformed active must fail closed")
	}
}

func TestMapUserIgnoringManagerKeepsOtherEnterpriseFields(t *testing.T) {
	u, err := ParseUser(userBody(t, map[string]any{
		"userName": "employee@example.com",
		SchemaEnterprise: map[string]any{
			"department": "Engineering",
			"manager":    nil,
		},
	}))
	if err != nil {
		t.Fatal(err)
	}
	mapped, err := u.MapUserIgnoringManager(false)
	if err != nil {
		t.Fatalf("manager outside the consumer contract must be ignored: %v", err)
	}
	if mapped.Fields[FieldDepartment] != "Engineering" {
		t.Fatalf("department=%#v", mapped.Fields[FieldDepartment])
	}
	if _, exists := mapped.Fields[FieldManagerID]; exists {
		t.Fatalf("manager leaked into fields: %#v", mapped.Fields)
	}
}

func TestApplyPatchIgnoringManagerKeepsNonManagerOperations(t *testing.T) {
	mapped, err := ApplyPatchIgnoringManager(patchOps(`[
		{"op":"replace","path":"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value","value":null},
		{"op":"replace","path":"title","value":"Engineer"}
	]`))
	if err != nil {
		t.Fatalf("manager outside the consumer contract must be ignored: %v", err)
	}
	if mapped.Fields[FieldTitle] != "Engineer" {
		t.Fatalf("title=%#v", mapped.Fields[FieldTitle])
	}
	if _, exists := mapped.Fields[FieldManagerID]; exists {
		t.Fatalf("manager leaked into fields: %#v", mapped.Fields)
	}
}

func TestMapUser_PutReplaceClearsAbsentAndRederivesLastName(t *testing.T) {
	u, _ := ParseUser(userBody(t, map[string]any{"userName": "renamer@pingcap.com", "active": true}))
	fm, err := u.MapUser(true, nil)
	if err != nil {
		t.Fatalf("map: %v", err)
	}
	if v, ok := fm.Fields[FieldTitle]; !ok || v != nil {
		t.Errorf("PUT should clear absent Title to nil, got %v ok=%v", v, ok)
	}
	if v, ok := fm.Fields[FieldFirstName]; !ok || v != nil {
		t.Errorf("PUT should clear absent FirstName to nil, got %v", v)
	}
	if got, _ := fm.Fields[FieldLastName].(string); got != "renamer" {
		t.Errorf("PUT should re-derive LastName to %q, got %q", "renamer", got)
	}
}

func TestMapUser_ManagerResolutionFailsClosed(t *testing.T) {
	body := userBody(t, map[string]any{
		"userName":       "x@pingcap.com",
		SchemaEnterprise: map[string]any{"manager": map[string]any{"value": "mgr@pingcap.com"}},
	})
	u, _ := ParseUser(body)
	if _, err := u.MapUser(false, neverResolve()); err == nil {
		t.Fatal("unresolved manager must fail closed (400), not silently clear")
	}
	fm, err := u.MapUser(false, alwaysResolve("005MANAGER"))
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if fm.Fields[FieldManagerID] != "005MANAGER" {
		t.Errorf("ManagerId = %v, want 005MANAGER", fm.Fields[FieldManagerID])
	}
	// An explicit empty (or whitespace) manager value fails closed — clearing is
	// only via PUT omission or PATCH remove, never a blank reference.
	for _, blank := range []string{"", "   "} {
		body = userBody(t, map[string]any{
			"userName":       "x@pingcap.com",
			SchemaEnterprise: map[string]any{"manager": map[string]any{"value": blank}},
		})
		u, _ = ParseUser(body)
		if _, err := u.MapUser(false, neverResolve()); err == nil {
			t.Fatalf("blank manager value %q must fail closed, not clear", blank)
		}
	}
	// A present-null manager attribute is malformed and fails closed.
	body = userBody(t, map[string]any{
		"userName":       "x@pingcap.com",
		SchemaEnterprise: map[string]any{"manager": nil},
	})
	u, _ = ParseUser(body)
	if _, err := u.MapUser(true, neverResolve()); err == nil {
		t.Fatal("null manager attribute must fail closed on PUT, not clear")
	}
}

func TestParseUserFilter(t *testing.T) {
	name, present, err := ParseUserFilter(`userName eq "a@b.co"`)
	if err != nil || !present || name != "a@b.co" {
		t.Fatalf("got %q present=%v err=%v", name, present, err)
	}
	if _, present, err := ParseUserFilter(""); err != nil || present {
		t.Fatalf("empty filter should be absent, no error")
	}
	if _, _, err := ParseUserFilter(`externalId eq "a@b.co"`); err == nil {
		t.Fatal("externalId filter must be rejected")
	}
	// RFC 7644 §3.4.2.2: attribute name and operator are case-insensitive.
	if v, _, err := ParseUserFilter(`USERNAME EQ "Mixed@Case.co"`); err != nil || v != "Mixed@Case.co" {
		t.Fatalf("case-insensitive filter failed: %q err=%v", v, err)
	}
	// Escaped quotes in the value must be unescaped, not truncate the match.
	if v, _, err := ParseUserFilter(`userName eq "a\"b@c.co"`); err != nil || v != `a"b@c.co` {
		t.Fatalf("escaped filter value failed: %q err=%v", v, err)
	}
}

func TestParseUserLookupFilter(t *testing.T) {
	field, value, present, err := ParseUserLookupFilter(`emails.value eq "a@b.co"`)
	if err != nil || !present || field != FieldEmail || value != "a@b.co" {
		t.Fatalf("email filter got field=%q value=%q present=%v err=%v", field, value, present, err)
	}
	field, value, present, err = ParseUserLookupFilter(`userName eq "a@b.co"`)
	if err != nil || !present || field != FieldUsername || value != "a@b.co" {
		t.Fatalf("username filter got field=%q value=%q present=%v err=%v", field, value, present, err)
	}
	if _, _, _, err := ParseUserLookupFilter(`externalId eq "a@b.co"`); err == nil {
		t.Fatal("externalId filter must be rejected")
	}
}

func TestApplyPatch_PathSpecificOpsAndActive(t *testing.T) {
	body := []byte(`{"schemas":["` + SchemaPatchOp + `"],"Operations":[
		{"op":"replace","path":"title","value":"VP"},
		{"op":"replace","path":"name.givenName","value":"Ming"},
		{"op":"replace","path":"` + SchemaEnterprise + `:department","value":"Sales"},
		{"op":"replace","path":"phoneNumbers[type eq \"mobile\"].value","value":"+86-9"},
		{"op":"replace","path":"active","value":false}
	]}`)
	fm, err := ApplyPatch(body, nil)
	if err != nil {
		t.Fatalf("patch: %v", err)
	}
	if fm.Fields[FieldTitle] != "VP" || fm.Fields[FieldFirstName] != "Ming" ||
		fm.Fields[FieldDepartment] != "Sales" || fm.Fields[FieldMobilePhone] != "+86-9" {
		t.Errorf("path ops mismapped: %+v", fm.Fields)
	}
	if fm.Active == nil || *fm.Active {
		t.Error("active=false not applied")
	}
}

func TestApplyPatch_RemoveAndUnknownPath(t *testing.T) {
	fm, err := ApplyPatch(patchOps(`[{"op":"remove","path":"title"}]`), nil)
	if err != nil {
		t.Fatalf("remove: %v", err)
	}
	if v, ok := fm.Fields[FieldTitle]; !ok || v != nil {
		t.Errorf("remove title should set nil, got %v", v)
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"bogus","value":"x"}]`), nil); err == nil {
		t.Fatal("unknown PATCH path must be rejected")
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"invalid","path":"title","value":"x"}]`), nil); err == nil {
		t.Fatal("unknown PATCH op must be rejected")
	}
}

func TestApplyPatch_IgnoresUnsupportedEnterpriseCompanyPaths(t *testing.T) {
	body := patchOps(`[
		{"op":"replace","path":"` + SchemaEnterprise + `:organization","value":"PingCAP"},
		{"op":"remove","path":"` + SchemaEnterprise + `:division"},
		{"op":"replace","path":"title","value":"VP"}
	]`)
	fm, err := ApplyPatch(body, nil)
	if err != nil {
		t.Fatalf("patch: %v", err)
	}
	if got := fm.Fields[FieldTitle]; got != "VP" {
		t.Errorf("title = %v, want VP", got)
	}
	for _, ignored := range []string{"Division", "CompanyName"} {
		if got, present := fm.Fields[ignored]; present {
			t.Errorf("%s must not be written, got %v", ignored, got)
		}
	}
}

func TestApplyPatch_RequiresSchemaAndOperations(t *testing.T) {
	// A body carrying Operations but not the PatchOp schema is not a SCIM PATCH.
	if _, err := ApplyPatch([]byte(`{"Operations":[{"op":"replace","path":"title","value":"x"}]}`), nil); err == nil {
		t.Fatal("PATCH without PatchOp schema must be rejected")
	}
	// A well-formed envelope with no operations is a no-op that must be rejected.
	if _, err := ApplyPatch(patchOps(`[]`), nil); err == nil {
		t.Fatal("PATCH with empty Operations must be rejected")
	}
	if _, err := ApplyPatch([]byte(`{}`), nil); err == nil {
		t.Fatal("empty object PATCH must be rejected")
	}
}

func TestApplyPatch_MissingValueFailsClosed(t *testing.T) {
	// replace/add of a scalar with no value must not clear the field.
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"title"}]`), nil); err == nil {
		t.Fatal("replace title with no value must fail closed")
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"title","value":null}]`), nil); err == nil {
		t.Fatal("replace title with null value must fail closed")
	}
	// manager replace with no/empty value must fail closed (use remove to clear).
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"`+SchemaEnterprise+`:manager"}]`), alwaysResolve("005X")); err == nil {
		t.Fatal("replace manager with no value must fail closed")
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"`+SchemaEnterprise+`:manager","value":{"value":""}}]`), alwaysResolve("005X")); err == nil {
		t.Fatal("replace manager with empty value must fail closed")
	}
	// remove manager is the sanctioned clear path.
	fm, err := ApplyPatch(patchOps(`[{"op":"remove","path":"`+SchemaEnterprise+`:manager"}]`), nil)
	if err != nil {
		t.Fatalf("remove manager: %v", err)
	}
	if v, ok := fm.Fields[FieldManagerID]; !ok || v != nil {
		t.Errorf("remove manager should clear to nil, got %v", v)
	}
}

func TestApplyPatch_DisplayNameNotMappedToLastName(t *testing.T) {
	// displayName must not overwrite the family name; the path is unsupported.
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"displayName","value":"Nick Name"}]`), nil); err == nil {
		t.Fatal("displayName PATCH must be rejected, not mapped to LastName")
	}
}

func TestMapUser_MinimalCreateDerivesLastName(t *testing.T) {
	// userName only, no name block — LastName is schema-required and must be
	// derived even on a non-replace create.
	u, err := ParseUser(userBody(t, map[string]any{"userName": "solo.user@pingcap.com"}))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	fm, err := u.MapUser(false, nil)
	if err != nil {
		t.Fatalf("map: %v", err)
	}
	if got, _ := fm.Fields[FieldLastName].(string); got != "solo.user" {
		t.Errorf("minimal create LastName = %q, want %q", got, "solo.user")
	}
}

func TestParseUser_RejectsNonStringIdentity(t *testing.T) {
	// userName as an object must fail closed, not be coerced to "map[]".
	if _, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"userName":{}}`)); err == nil {
		t.Fatal("object userName must be rejected")
	}
	// numeric email value must fail closed.
	if _, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"emails":[{"value":123,"primary":true}]}`)); err == nil {
		t.Fatal("numeric email must be rejected")
	}
}

func TestMapUser_RejectsNonStringField(t *testing.T) {
	// A structurally valid identity but an object-valued title must fail closed
	// rather than write a coerced string and (on PUT) clear managed fields.
	body := userBody(t, map[string]any{"userName": "ok@pingcap.com", "title": map[string]any{}})
	u, err := ParseUser(body)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if _, err := u.MapUser(true, nil); err == nil {
		t.Fatal("object-valued title must be rejected")
	}
}

func TestMapUser_MalformedManagerFailsClosed(t *testing.T) {
	// Manager object present but missing value must fail closed (not silent clear).
	body := userBody(t, map[string]any{
		"userName":       "x@pingcap.com",
		SchemaEnterprise: map[string]any{"manager": map[string]any{}},
	})
	u, _ := ParseUser(body)
	if _, err := u.MapUser(true, alwaysResolve("005X")); err == nil {
		t.Fatal("manager object without value must fail closed")
	}
	// Manager value as a non-string must fail closed.
	body = userBody(t, map[string]any{
		"userName":       "x@pingcap.com",
		SchemaEnterprise: map[string]any{"manager": map[string]any{"value": 42}},
	})
	u, _ = ParseUser(body)
	if _, err := u.MapUser(false, alwaysResolve("005X")); err == nil {
		t.Fatal("non-string manager value must fail closed")
	}
}

func TestApplyPatch_EmptyPathPartialSet(t *testing.T) {
	// The canonical deactivation form: an empty-path object carrying only active.
	fm, err := ApplyPatch([]byte(`{"schemas":["`+SchemaPatchOp+`"],"Operations":[
		{"op":"replace","value":{"active":false}}
	]}`), nil)
	if err != nil {
		t.Fatalf("empty-path patch: %v", err)
	}
	if fm.Active == nil || *fm.Active {
		t.Fatal("empty-path {active:false} must deactivate")
	}
	// Empty-path object with several attributes maps them all.
	fm, err = ApplyPatch(patchOps(`[
		{"op":"replace","path":"","value":{"title":"Lead","`+SchemaEnterprise+`":{"department":"Ops"}}}
	]`), nil)
	if err != nil {
		t.Fatalf("empty-path multi: %v", err)
	}
	if fm.Fields[FieldTitle] != "Lead" || fm.Fields[FieldDepartment] != "Ops" {
		t.Errorf("empty-path multi mismapped: %+v", fm.Fields)
	}
}

func TestApplyPatch_RejectsNonStringValue(t *testing.T) {
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"title","value":{}}]`), nil); err == nil {
		t.Fatal("object PATCH value for a scalar path must be rejected")
	}
}

func TestMapUser_EmailOnlyPopulatesUsername(t *testing.T) {
	// An email-only SCIM payload must still yield a Username (login identity),
	// mirrored from the email, so the created User is addressable.
	body := userBody(t, map[string]any{
		"emails": []any{map[string]any{"value": "only.email@pingcap.com", "primary": true}},
	})
	u, err := ParseUser(body)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	fm, err := u.MapUser(false, nil)
	if err != nil {
		t.Fatalf("map: %v", err)
	}
	if fm.Fields[FieldUsername] != "only.email@pingcap.com" {
		t.Errorf("Username = %v, want mirror of email", fm.Fields[FieldUsername])
	}
	if fm.Fields[FieldEmail] != "only.email@pingcap.com" {
		t.Errorf("Email = %v", fm.Fields[FieldEmail])
	}
}

func TestApplyPatch_NonStringPathFailsClosed(t *testing.T) {
	// path:{} must not collapse to "" and be treated as a whole-resource op.
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":{},"value":{"title":"X"}}]`), nil); err == nil {
		t.Fatal("non-string PATCH path must be rejected")
	}
	// A present null path must also fail closed (not normalize to whole-resource).
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":null,"value":{"active":false}}]`), nil); err == nil {
		t.Fatal("null PATCH path must be rejected")
	}
	// An omitted path (whole-resource op) is still valid.
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"active":false}}]`), nil); err != nil {
		t.Fatalf("omitted-path whole-resource op should be valid: %v", err)
	}
}

func TestMapUser_NullManagerValueFailsClosed(t *testing.T) {
	body := userBody(t, map[string]any{
		"userName":       "x@pingcap.com",
		SchemaEnterprise: map[string]any{"manager": map[string]any{"value": nil}},
	})
	u, _ := ParseUser(body)
	if _, err := u.MapUser(true, alwaysResolve("005X")); err == nil {
		t.Fatal("null manager value must fail closed, not clear the manager")
	}
}

func TestApplyPatch_BlankIdentityFieldsRejected(t *testing.T) {
	for _, p := range []string{"userName", "emails.value", "name.familyName"} {
		body := patchOps(`[{"op":"replace","path":"` + p + `","value":""}]`)
		if _, err := ApplyPatch(body, nil); err == nil {
			t.Errorf("blank %s via PATCH must be rejected", p)
		}
		rm := patchOps(`[{"op":"remove","path":"` + p + `"}]`)
		if _, err := ApplyPatch(rm, nil); err == nil {
			t.Errorf("removing %s via PATCH must be rejected", p)
		}
	}
}

func TestApplyPatch_NameComplexMergeSemantics(t *testing.T) {
	// PATCH on complex `name` merges supplied sub-attributes; omitted ones are
	// left unchanged (RFC 7644 §3.5.2.3), not cleared.
	// replace with only familyName -> LastName set, FirstName untouched.
	fm, err := ApplyPatch(patchOps(`[{"op":"replace","path":"name","value":{"familyName":"Wu"}}]`), nil)
	if err != nil {
		t.Fatalf("name replace: %v", err)
	}
	if fm.Fields[FieldLastName] != "Wu" {
		t.Errorf("LastName = %v, want Wu", fm.Fields[FieldLastName])
	}
	if _, ok := fm.Fields[FieldFirstName]; ok {
		t.Error("omitted givenName must be left unchanged, not cleared")
	}
	// replace with only givenName is valid (familyName untouched, not required).
	fm, err = ApplyPatch(patchOps(`[{"op":"replace","path":"name","value":{"givenName":"Ming"}}]`), nil)
	if err != nil {
		t.Fatalf("name replace givenName-only should be accepted: %v", err)
	}
	if fm.Fields[FieldFirstName] != "Ming" {
		t.Errorf("FirstName = %v, want Ming", fm.Fields[FieldFirstName])
	}
	if _, ok := fm.Fields[FieldLastName]; ok {
		t.Error("omitted familyName must be left unchanged")
	}
	// givenName:"" explicitly clears FirstName; familyName:"" is rejected (required).
	fm, err = ApplyPatch(patchOps(`[{"op":"replace","path":"name","value":{"givenName":""}}]`), nil)
	if err != nil {
		t.Fatalf("givenName clear: %v", err)
	}
	if v, ok := fm.Fields[FieldFirstName]; !ok || v != nil {
		t.Errorf("givenName:\"\" should clear FirstName to nil, got ok=%v v=%v", ok, v)
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","path":"name","value":{"familyName":""}}]`), nil); err == nil {
		t.Fatal("familyName:\"\" must be rejected (LastName required)")
	}
}

func TestApplyPatch_EmailPaths(t *testing.T) {
	// Only the explicitly-primary selector maps to the single login Email.
	{
		body := patchOpsJSON(t, map[string]any{"op": "replace", "path": `emails[primary eq true].value`, "value": "new@pingcap.com"})
		fm, err := ApplyPatch(body, nil)
		if err != nil {
			t.Fatalf("primary email path: %v", err)
		}
		if fm.Fields[FieldEmail] != "new@pingcap.com" {
			t.Errorf("primary email path -> %v", fm.Fields[FieldEmail])
		}
	}
	// Ambiguous / type-selected addresses are NOT the login and must be rejected,
	// so a non-primary address can never overwrite User.Email.
	for _, p := range []string{"emails.value", `emails[type eq "work"].value`, `emails[type eq "home"].value`, `emails[type eq "other"].value`} {
		body := patchOpsJSON(t, map[string]any{"op": "replace", "path": p, "value": "x@pingcap.com"})
		if _, err := ApplyPatch(body, nil); err == nil {
			t.Errorf("ambiguous email path %q must be rejected", p)
		}
	}
	// complex emails array replace -> primary email.
	fm, err := ApplyPatch(patchOps(`[{"op":"replace","path":"emails","value":[{"value":"a@x.co"},{"value":"b@x.co","primary":true}]}]`), nil)
	if err != nil {
		t.Fatalf("emails array: %v", err)
	}
	if fm.Fields[FieldEmail] != "b@x.co" {
		t.Errorf("emails array primary = %v, want b@x.co", fm.Fields[FieldEmail])
	}
}

func TestPrimaryEmail_MalformedFlagFailsClosed(t *testing.T) {
	// primary:"yes" is neither bool nor true/false -> must fail closed, not be
	// silently ignored (which would pick the wrong address).
	body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":"a@x.co"},{"value":"b@x.co","primary":"yes"}]}`)
	if _, err := ParseUser(body); err == nil {
		t.Fatal("malformed primary flag must be rejected")
	}
	// primary:"true" (string) is accepted and selects that address.
	u, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":"a@x.co"},{"value":"b@x.co","primary":"true"}]}`))
	if err != nil {
		t.Fatalf("string true primary: %v", err)
	}
	fm, _ := u.MapUser(false, nil)
	if fm.Fields[FieldEmail] != "b@x.co" {
		t.Errorf("primary string true should select b@x.co, got %v", fm.Fields[FieldEmail])
	}
}

func TestMapUser_PhoneSelectionByTypeNotOrder(t *testing.T) {
	// A home number listed before work must not become Phone and drop work.
	body := userBody(t, map[string]any{
		"userName": "p@pingcap.com",
		"phoneNumbers": []any{
			map[string]any{"type": "home", "value": "+home"},
			map[string]any{"type": "work", "value": "+work"},
			map[string]any{"type": "mobile", "value": "+mobile"},
		},
	})
	u, _ := ParseUser(body)
	fm, err := u.MapUser(false, nil)
	if err != nil {
		t.Fatalf("map: %v", err)
	}
	if fm.Fields[FieldPhone] != "+work" {
		t.Errorf("Phone should be the work number regardless of order, got %v", fm.Fields[FieldPhone])
	}
	if fm.Fields[FieldMobilePhone] != "+mobile" {
		t.Errorf("MobilePhone = %v", fm.Fields[FieldMobilePhone])
	}
}

func TestCaseInsensitiveAttributes(t *testing.T) {
	// Differently-cased attribute names must resolve (RFC 7643 §2.1).
	body := []byte(`{"schemas":["` + SchemaUser + `"],"UserName":"c@pingcap.com","Active":false,
		"Name":{"GivenName":"Cee","FamilyName":"Case"},
		"` + SchemaEnterprise + `":{"Department":"Ops","Manager":{"Value":"mgr@pingcap.com"}}}`)
	u, err := ParseUser(body)
	if err != nil {
		t.Fatalf("parse mixed-case: %v", err)
	}
	fm, err := u.MapUser(false, alwaysResolve("005MGR"))
	if err != nil {
		t.Fatalf("map mixed-case: %v", err)
	}
	if fm.Fields[FieldUsername] != "c@pingcap.com" || fm.Fields[FieldFirstName] != "Cee" ||
		fm.Fields[FieldLastName] != "Case" || fm.Fields[FieldDepartment] != "Ops" ||
		fm.Fields[FieldManagerID] != "005MGR" {
		t.Errorf("mixed-case attributes mismapped: %+v", fm.Fields)
	}
	if fm.Active == nil || *fm.Active {
		t.Error("mixed-case Active:false must deactivate")
	}
}

func TestParseUser_NullAndMalformedEmailFailClosed(t *testing.T) {
	cases := []string{
		`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":null}]}`,
		`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":""}]}`,
		`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":42}]}`,
	}
	for _, body := range cases {
		if _, err := ParseUser([]byte(body)); err == nil {
			t.Errorf("malformed email value must be rejected: %s", body)
		}
	}
	// A null userName is rejected even when a valid email is present (so Email
	// can never be silently mirrored into a nulled identity).
	if _, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"userName":null,"emails":[{"value":"ok@pingcap.com","primary":true}]}`)); err == nil {
		t.Fatal("null userName must be rejected")
	}
	// When emails is ABSENT entirely, Email falls back to userName so the
	// required contact field is populated on a create.
	u, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com"}`))
	if err != nil {
		t.Fatalf("userName-only should parse: %v", err)
	}
	fm, _ := u.MapUser(false, nil)
	if fm.Fields[FieldEmail] != "x@pingcap.com" {
		t.Errorf("Email should fall back to userName when emails absent, got %v", fm.Fields[FieldEmail])
	}
	// A value-less email entry is malformed and fails closed at parse time
	// (rather than being skipped and leaving Email stale/unset on a PUT).
	if _, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"type":"work"}]}`)); err == nil {
		t.Fatal("value-less email entry must be rejected")
	}
}

func TestParseUser_NullPrimaryFlagFailsClosed(t *testing.T) {
	// emails primary:null must fail closed, not be treated as "not primary"
	// (which could select an arbitrary fallback address).
	body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":"a@x.co","primary":null}]}`)
	if _, err := ParseUser(body); err == nil {
		t.Fatal("null primary flag must be rejected")
	}
}

func TestParseUser_AmbiguousCaseKeysRejected(t *testing.T) {
	// Two keys differing only in case would resolve nondeterministically.
	body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"a@x.co","USERNAME":"b@x.co"}`)
	if _, err := ParseUser(body); err == nil {
		t.Fatal("ambiguous case-variant keys must be rejected")
	}
	// Nested ambiguity is caught too.
	nested := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"a@x.co","name":{"givenName":"A","GIVENNAME":"B"}}`)
	if _, err := ParseUser(nested); err == nil {
		t.Fatal("nested ambiguous keys must be rejected")
	}
}

func TestApplyPatch_EmptyPathClearsAndGuards(t *testing.T) {
	// An explicit empty string in an empty-path object clears a clearable field.
	fm, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"title":""}}]`), nil)
	if err != nil {
		t.Fatalf("empty-path clear title: %v", err)
	}
	if v, ok := fm.Fields[FieldTitle]; !ok || v != nil {
		t.Errorf("empty-path title:\"\" should clear Title to nil, got ok=%v v=%v", ok, v)
	}
	// But an empty required/identity attribute is rejected, not silently dropped.
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"userName":""}}]`), nil); err == nil {
		t.Fatal("empty-path userName:\"\" must be rejected")
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"name":{"familyName":""}}}]`), nil); err == nil {
		t.Fatal("empty-path familyName:\"\" must be rejected")
	}
}

func TestParseUser_EmptyUserNameRejected(t *testing.T) {
	// A present-but-empty userName must be rejected, not mirrored from email.
	for _, un := range []string{`""`, `"   "`} {
		body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":` + un + `,"emails":[{"value":"a@x.co","primary":true}]}`)
		if _, err := ParseUser(body); err == nil {
			t.Errorf("empty userName %s must be rejected", un)
		}
	}
}

func TestPrimaryEmail_ValidatesAllEntries(t *testing.T) {
	// A valid primary followed by a malformed entry must still fail closed.
	body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":"ok@x.co","primary":true},{"value":null}]}`)
	if _, err := ParseUser(body); err == nil {
		t.Fatal("a malformed email after the primary must still be rejected")
	}
}

func TestApplyPatch_PhoneNumbersReplaceClears(t *testing.T) {
	// A replace with an empty phoneNumbers array clears both phone fields.
	fm, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"phoneNumbers":[]}}]`), nil)
	if err != nil {
		t.Fatalf("phone clear: %v", err)
	}
	for _, f := range []string{FieldPhone, FieldMobilePhone} {
		if v, ok := fm.Fields[f]; !ok || v != nil {
			t.Errorf("empty phoneNumbers replace should clear %s to nil, got ok=%v v=%v", f, ok, v)
		}
	}
	// A replace with only a mobile clears the (absent) work Phone.
	fm, err = ApplyPatch(patchOps(`[{"op":"replace","value":{"phoneNumbers":[{"type":"mobile","value":"+1"}]}}]`), nil)
	if err != nil {
		t.Fatalf("phone mobile-only: %v", err)
	}
	if fm.Fields[FieldMobilePhone] != "+1" {
		t.Errorf("MobilePhone = %v", fm.Fields[FieldMobilePhone])
	}
	if v, ok := fm.Fields[FieldPhone]; !ok || v != nil {
		t.Errorf("absent work number should clear Phone to nil, got ok=%v v=%v", ok, v)
	}
}

func TestApplyPatch_AddMultiValuedFailsClosed(t *testing.T) {
	// `add` of a multi-valued attribute via empty-path can't append to a single
	// CRM field, so it must fail closed rather than clobber.
	if _, err := ApplyPatch(patchOps(`[{"op":"add","value":{"emails":[{"value":"a@x.co"}]}}]`), nil); err == nil {
		t.Fatal("add emails via empty-path must be rejected")
	}
	if _, err := ApplyPatch(patchOps(`[{"op":"add","value":{"phoneNumbers":[{"type":"work","value":"+1"}]}}]`), nil); err == nil {
		t.Fatal("add phoneNumbers via empty-path must be rejected")
	}
	// `add` of a scalar is fine and does NOT clear on empty (unlike replace).
	fm, err := ApplyPatch(patchOps(`[{"op":"add","value":{"title":"Lead"}}]`), nil)
	if err != nil || fm.Fields[FieldTitle] != "Lead" {
		t.Fatalf("add scalar title failed: %v (%v)", err, fm.Fields[FieldTitle])
	}
}

func TestApplyPatch_AddMultiValuedPathFailsClosed(t *testing.T) {
	// `add` on the email/phone paths can't append to the single CRM field.
	for _, p := range []string{"emails.value", `emails[primary eq true].value`, `phoneNumbers[type eq "work"].value`} {
		body := patchOpsJSON(t, map[string]any{"op": "add", "path": p, "value": "x@pingcap.com"})
		if _, err := ApplyPatch(body, nil); err == nil {
			t.Errorf("add on multi-valued path %q must be rejected", p)
		}
	}
	// `add` on the complex emails array is likewise rejected.
	if _, err := ApplyPatch(patchOps(`[{"op":"add","path":"emails","value":[{"value":"a@x.co"}]}]`), nil); err == nil {
		t.Fatal("add on complex emails must be rejected")
	}
	// `replace` on the primary selector is still fine.
	if _, err := ApplyPatch(patchOpsJSON(t, map[string]any{"op": "replace", "path": `emails[primary eq true].value`, "value": "y@pingcap.com"}), nil); err != nil {
		t.Fatalf("replace on primary email should be accepted: %v", err)
	}
}

func TestMapUser_MalformedPhoneFailsClosed(t *testing.T) {
	// A malformed phone value on a PUT must fail closed, not be skipped (which
	// would let the managed-field loop delete the phone).
	for _, entry := range []string{`{"value":null}`, `{"value":""}`, `{"type":"work"}`, `{"value":42}`} {
		body := userBody(t, map[string]any{"userName": "x@pingcap.com"})
		// rebuild with phoneNumbers containing the malformed entry
		body = []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","phoneNumbers":[` + entry + `]}`)
		u, err := ParseUser(body)
		if err == nil {
			_, err = u.MapUser(true, nil)
		}
		if err == nil {
			t.Errorf("malformed phone entry %s must fail closed", entry)
		}
	}
}

func TestMapUser_NullEnterpriseFailsClosed(t *testing.T) {
	body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","` + SchemaEnterprise + `":null}`)
	u, err := ParseUser(body)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if _, err := u.MapUser(true, nil); err == nil {
		t.Fatal("present-null enterprise extension must fail closed on PUT")
	}
}

func TestMapUser_PresentNullAttributesFailClosed(t *testing.T) {
	// A present-null string attribute is malformed and must fail closed on a PUT
	// rather than be read as empty and let the managed-field loop clear the field.
	cases := []string{
		`"title":null`,
		`"name":{"givenName":null}`,
		`"` + SchemaEnterprise + `":{"department":null}`,
	}
	for _, c := range cases {
		body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com",` + c + `}`)
		u, err := ParseUser(body)
		if err == nil {
			_, err = u.MapUser(true, nil)
		}
		if err == nil {
			t.Errorf("present-null attribute %q must fail closed", c)
		}
	}
}

func TestApplyPatch_ComplexPhoneNumbers(t *testing.T) {
	// replace phoneNumbers array maps to Phone/MobilePhone.
	fm, err := ApplyPatch(patchOps(`[{"op":"replace","path":"phoneNumbers","value":[{"type":"work","value":"+w"},{"type":"mobile","value":"+m"}]}]`), nil)
	if err != nil {
		t.Fatalf("phoneNumbers replace: %v", err)
	}
	if fm.Fields[FieldPhone] != "+w" || fm.Fields[FieldMobilePhone] != "+m" {
		t.Errorf("phoneNumbers replace mismapped: %+v", fm.Fields)
	}
	// remove phoneNumbers clears both.
	fm, err = ApplyPatch(patchOps(`[{"op":"remove","path":"phoneNumbers"}]`), nil)
	if err != nil {
		t.Fatalf("phoneNumbers remove: %v", err)
	}
	for _, f := range []string{FieldPhone, FieldMobilePhone} {
		if v, ok := fm.Fields[f]; !ok || v != nil {
			t.Errorf("remove phoneNumbers should clear %s, got ok=%v v=%v", f, ok, v)
		}
	}
	// add phoneNumbers array is rejected.
	if _, err := ApplyPatch(patchOps(`[{"op":"add","path":"phoneNumbers","value":[{"type":"work","value":"+w"}]}]`), nil); err == nil {
		t.Fatal("add complex phoneNumbers must be rejected")
	}
}

func TestMapUser_PresentEmptyEmailsFailClosed(t *testing.T) {
	// A present but empty emails array yields no usable address. Since Email is
	// required, this fails closed (rather than mirroring userName or leaving
	// Email stale/unset on a PUT).
	u, err := ParseUser([]byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[]}`))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if _, err := u.MapUser(true, nil); err == nil {
		t.Fatal("present empty emails must fail closed (Email required)")
	}
}

func TestMapUser_NullComplexAttributesFailClosed(t *testing.T) {
	for _, attr := range []string{`"emails":null`, `"phoneNumbers":null`, `"name":null`} {
		body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com",` + attr + `}`)
		u, err := ParseUser(body)
		if err == nil {
			_, err = u.MapUser(true, nil)
		}
		if err == nil {
			t.Errorf("present-null complex attribute %q must fail closed", attr)
		}
	}
}

func TestParseActive_StrictStringsOnly(t *testing.T) {
	// Exact "true"/"false" strings are accepted.
	for lit, want := range map[string]bool{"false": false, "true": true} {
		u, _ := ParseUser(userBody(t, map[string]any{"userName": "a@b.co", "active": lit}))
		fm, err := u.MapUser(false, nil)
		if err != nil || fm.Active == nil || *fm.Active != want {
			t.Errorf("active=%q should map to %v, got %v err=%v", lit, want, fm.Active, err)
		}
	}
	// Case/whitespace variants and other types fail closed so a malformed value
	// can't deactivate a user.
	for _, bad := range []string{`" FALSE "`, `"FALSE"`, `"False"`, `"true "`, `"1"`, `"yes"`} {
		body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"a@b.co","active":` + bad + `}`)
		u, err := ParseUser(body)
		if err == nil {
			_, err = u.MapUser(false, nil)
		}
		if err == nil {
			t.Errorf("malformed active %s must be rejected", bad)
		}
	}
}

func TestParseUser_MultiplePrimaryEmailsRejected(t *testing.T) {
	body := []byte(`{"schemas":["` + SchemaUser + `"],"userName":"x@pingcap.com","emails":[{"value":"a@x.co","primary":true},{"value":"b@x.co","primary":true}]}`)
	if _, err := ParseUser(body); err == nil {
		t.Fatal("multiple primary emails must be rejected (ambiguous login)")
	}
}

func TestApplyPatch_CoreURNQualifiedPath(t *testing.T) {
	// A core-User-URN-qualified path must normalize the same as the bare path.
	fm, err := ApplyPatch(patchOpsJSON(t, map[string]any{
		"op": "replace", "path": SchemaUser + ":title", "value": "VP",
	}), nil)
	if err != nil {
		t.Fatalf("core-URN path: %v", err)
	}
	if fm.Fields[FieldTitle] != "VP" {
		t.Errorf("core-URN qualified title = %v, want VP", fm.Fields[FieldTitle])
	}
}

func TestApplyPatch_EmptyPathEmptyEmailsFailsClosed(t *testing.T) {
	// An empty-path replace with emails:[] can't clear the required Email; it must
	// fail closed (not report success and silently leave Email stale), and must
	// not partially apply other attributes in the same op.
	if _, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"title":"VP","emails":[]}}]`), nil); err == nil {
		t.Fatal("empty-path replace with emails:[] must fail closed")
	}
	// A complex emails replace targeting the whole attribute with [] is likewise
	// rejected (already covered), and a valid empty-path replace still works.
	fm, err := ApplyPatch(patchOps(`[{"op":"replace","value":{"emails":[{"value":"ok@x.co","primary":true}]}}]`), nil)
	if err != nil {
		t.Fatalf("valid empty-path emails replace: %v", err)
	}
	if fm.Fields[FieldEmail] != "ok@x.co" {
		t.Errorf("Email = %v, want ok@x.co", fm.Fields[FieldEmail])
	}
}

func TestErrorBody(t *testing.T) {
	e := newError(404, "nope", "")
	b := e.ErrorBody()
	if b["status"] != "404" || b["detail"] != "nope" {
		t.Errorf("error body wrong: %+v", b)
	}
	if _, ok := b["scimType"]; ok {
		t.Error("scimType should be omitted when empty")
	}
}
