package layoutroutingsecurityv1

import (
	"fmt"
	"reflect"
	"strings"
	"testing"
)

func TestViewOwnsOnlyRoutingIdentityAndTopology(t *testing.T) {
	assertStructFields(t, reflect.TypeOf(ProfileKey{}), []string{"NamespaceID", "LocalKey"})
	assertStructFields(t, reflect.TypeOf(RoleKey{}), []string{"NamespaceID", "LocalKey"})
	assertStructFields(t, reflect.TypeOf(ProfileIdentity{}), []string{"Key", "ResolvedProfileID", "State"})
	assertStructFields(t, reflect.TypeOf(RoleIdentity{}), []string{"Key", "ResolvedRoleID", "State", "ParentRoleKey"})
	assertStructFields(t, reflect.TypeOf(View{}), []string{
		"OrgID", "CanonicalizationVersion", "KeyGrammarVersion",
		"RoleTopologyGrammarVersion", "Profiles", "Roles",
	})
	if reflect.TypeOf(ProfileKey{}) == reflect.TypeOf(RoleKey{}) {
		t.Fatal("ProfileKey and RoleKey lost their separate type domains")
	}
}

func TestValidateStableKeysRejectsRatherThanNormalizes(t *testing.T) {
	if err := ValidateProfileKey(ProfileKey{NamespaceID: "io.example", LocalKey: "support.user"}); err != nil {
		t.Fatalf("valid ProfileKey rejected: %v", err)
	}
	if err := ValidateRoleKey(RoleKey{NamespaceID: "tenant.example", LocalKey: "sales-manager"}); err != nil {
		t.Fatalf("valid RoleKey rejected: %v", err)
	}

	invalidProfiles := []ProfileKey{
		{},
		{NamespaceID: "single", LocalKey: "profile"},
		{NamespaceID: "IO.example", LocalKey: "profile"},
		{NamespaceID: "io.example", LocalKey: "Profile"},
		{NamespaceID: "io.example", LocalKey: " profile"},
		{NamespaceID: "io.example", LocalKey: "profile-"},
		{NamespaceID: "io.example", LocalKey: "profile..child"},
	}
	for _, key := range invalidProfiles {
		if err := ValidateProfileKey(key); err == nil {
			t.Fatalf("invalid ProfileKey accepted: %#v", key)
		}
	}
}

func TestStableKeyAndResolvedIDBounds(t *testing.T) {
	maxNamespace := strings.Repeat("a", 63) + "." + strings.Repeat("b", 62) + ".c"
	maxLocalKey := strings.Repeat("d", 63) + "." + strings.Repeat("e", 62) + ".f"
	if len(maxNamespace) != MaxNamespaceIDBytes {
		t.Fatalf("namespace boundary fixture length = %d", len(maxNamespace))
	}
	if len(maxLocalKey) != MaxLocalKeyBytes {
		t.Fatalf("local-key boundary fixture length = %d", len(maxLocalKey))
	}
	if err := ValidateProfileKey(ProfileKey{NamespaceID: maxNamespace, LocalKey: maxLocalKey}); err != nil {
		t.Fatalf("maximum key rejected: %v", err)
	}
	tooLong := strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + ".c"
	if err := ValidateProfileKey(ProfileKey{NamespaceID: tooLong, LocalKey: "profile"}); err == nil {
		t.Fatal("overlong namespace accepted")
	}
	if err := ValidateProfileKey(ProfileKey{NamespaceID: "io.example", LocalKey: strings.Repeat("a", MaxKeySegmentBytes+1)}); err == nil {
		t.Fatal("overlong key segment accepted")
	}

	view := validView()
	view.Profiles[0].ResolvedProfileID = strings.Repeat("x", MaxResolvedIDBytes)
	if err := ValidateView(view); err != nil {
		t.Fatalf("maximum resolved ID rejected: %v", err)
	}
	view.Profiles[0].ResolvedProfileID += "x"
	if err := ValidateView(view); err == nil {
		t.Fatal("overlong resolved ID accepted")
	}
}

func TestValidateReusableOrgAndLocalIdentityAtoms(t *testing.T) {
	if err := ValidateOrgID("组织-example"); err != nil {
		t.Fatalf("valid multibyte org rejected: %v", err)
	}
	if err := ValidateResolvedLocalID("本地-profile"); err != nil {
		t.Fatalf("valid multibyte local ID rejected: %v", err)
	}
	for _, orgID := range []string{"", " org", "org ", "org\n1"} {
		if err := ValidateOrgID(orgID); err == nil {
			t.Fatalf("invalid org accepted: %q", orgID)
		}
	}
	for _, localID := range []string{"", " local", "local ", "local\x7f1"} {
		if err := ValidateResolvedLocalID(localID); err == nil {
			t.Fatalf("invalid local ID accepted: %q", localID)
		}
	}
}

func TestValidateViewAcceptsCompleteForestAndTombstones(t *testing.T) {
	if err := ValidateView(validView()); err != nil {
		t.Fatalf("valid view rejected: %v", err)
	}
	view := validView()
	view.Profiles[0].ResolvedProfileID = view.Roles[0].ResolvedRoleID
	if err := ValidateView(view); err != nil {
		t.Fatalf("same opaque ID bytes across typed dimensions rejected: %v", err)
	}
}

func TestValidateViewRejectsIdentityCountOverflowBeforeEntries(t *testing.T) {
	view := validView()
	view.Profiles = make([]ProfileIdentity, MaxProfileIdentities+1)
	if err := ValidateView(view); err == nil {
		t.Fatal("Profile count overflow accepted")
	}
	view = validView()
	view.Roles = make([]RoleIdentity, MaxRoleIdentities+1)
	if err := ValidateView(view); err == nil {
		t.Fatal("Role count overflow accepted")
	}
}

func TestValidateViewRejectsIncompleteAmbiguousOrInvalidTopology(t *testing.T) {
	tests := map[string]func(*View){
		"blank org": func(value *View) { value.OrgID = "" },
		"unsupported canonicalization": func(value *View) {
			value.CanonicalizationVersion = "v2"
		},
		"unsupported key grammar": func(value *View) { value.KeyGrammarVersion = "v2" },
		"unsupported topology grammar": func(value *View) {
			value.RoleTopologyGrammarVersion = "v2"
		},
		"missing profiles": func(value *View) { value.Profiles = nil },
		"missing roles":    func(value *View) { value.Roles = nil },
		"invalid profile key": func(value *View) {
			value.Profiles[0].Key.LocalKey = "Support"
		},
		"invalid profile id": func(value *View) {
			value.Profiles[0].ResolvedProfileID = " profile"
		},
		"invalid profile state": func(value *View) {
			value.Profiles[0].State = "INACTIVE"
		},
		"duplicate profile key": func(value *View) {
			value.Profiles[1].Key = value.Profiles[0].Key
		},
		"duplicate profile id": func(value *View) {
			value.Profiles[1].ResolvedProfileID = value.Profiles[0].ResolvedProfileID
		},
		"invalid role key": func(value *View) { value.Roles[0].Key.NamespaceID = "Tenant.example" },
		"invalid role id":  func(value *View) { value.Roles[0].ResolvedRoleID = "role\n1" },
		"invalid role state": func(value *View) {
			value.Roles[0].State = "DELETED"
		},
		"duplicate role key": func(value *View) { value.Roles[1].Key = value.Roles[0].Key },
		"duplicate role id": func(value *View) {
			value.Roles[1].ResolvedRoleID = value.Roles[0].ResolvedRoleID
		},
		"tombstone with parent": func(value *View) {
			parent := value.Roles[1].Key
			value.Roles[2].ParentRoleKey = &parent
		},
		"missing parent": func(value *View) {
			value.Roles[0].ParentRoleKey = &RoleKey{NamespaceID: "tenant.example", LocalKey: "missing"}
		},
		"tombstoned parent": func(value *View) {
			parent := value.Roles[2].Key
			value.Roles[0].ParentRoleKey = &parent
		},
		"self parent": func(value *View) {
			parent := value.Roles[0].Key
			value.Roles[0].ParentRoleKey = &parent
		},
		"cycle": func(value *View) {
			parent := value.Roles[0].Key
			value.Roles[1].ParentRoleKey = &parent
		},
	}

	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			view := validView()
			mutate(&view)
			if err := ValidateView(view); err == nil {
				t.Fatal("invalid view accepted")
			}
			if canonical, err := CanonicalViewBytes(view); err == nil || canonical != nil {
				t.Fatalf("invalid view acquired canonical bytes: %x, err=%v", canonical, err)
			}
			if digest, err := ViewDigest(view); err == nil || digest != "" {
				t.Fatalf("invalid view acquired digest %q, err=%v", digest, err)
			}
		})
	}
}

func TestCanonicalizeViewIsPermutationInvariantAndDefensive(t *testing.T) {
	view := validView()
	digest, err := ViewDigest(view)
	if err != nil {
		t.Fatal(err)
	}
	view.Profiles[0], view.Profiles[1] = view.Profiles[1], view.Profiles[0]
	view.Roles[0], view.Roles[2] = view.Roles[2], view.Roles[0]
	permutedDigest, err := ViewDigest(view)
	if err != nil {
		t.Fatal(err)
	}
	if permutedDigest != digest {
		t.Fatalf("permutation changed digest: %s != %s", permutedDigest, digest)
	}

	canonical, err := CanonicalizeView(view)
	if err != nil {
		t.Fatal(err)
	}
	for index := 1; index < len(canonical.Profiles); index++ {
		if !profileKeyLess(canonical.Profiles[index-1].Key, canonical.Profiles[index].Key) {
			t.Fatal("canonical Profiles are not strictly sorted")
		}
	}
	for index := 1; index < len(canonical.Roles); index++ {
		if !roleKeyLess(canonical.Roles[index-1].Key, canonical.Roles[index].Key) {
			t.Fatal("canonical Roles are not strictly sorted")
		}
	}
	view.Profiles[0].Key.LocalKey = "changed"
	if canonical.Roles[2].ParentRoleKey != nil {
		view.Roles[2].ParentRoleKey.LocalKey = "changed"
	}
	if canonical.Profiles[0].Key.LocalKey == "changed" {
		t.Fatal("canonical Profiles alias caller storage")
	}
	for _, identity := range canonical.Roles {
		if identity.ParentRoleKey != nil && identity.ParentRoleKey.LocalKey == "changed" {
			t.Fatal("canonical parent key aliases caller storage")
		}
	}
}

func TestViewDigestCommitsEverySemanticContentClass(t *testing.T) {
	base := validView()
	baseDigest, err := ViewDigest(base)
	if err != nil {
		t.Fatal(err)
	}
	mutations := map[string]func(*View){
		"org":         func(value *View) { value.OrgID = "org-2" },
		"profile key": func(value *View) { value.Profiles[0].Key.LocalKey = "support-admin" },
		"profile id":  func(value *View) { value.Profiles[0].ResolvedProfileID = "00e-other" },
		"profile state": func(value *View) {
			value.Profiles[0].State = IdentityStateTombstone
		},
		"role id":      func(value *View) { value.Roles[0].ResolvedRoleID = "role-other" },
		"role state":   func(value *View) { value.Roles[2].State = IdentityStateActive },
		"parent graph": func(value *View) { value.Roles[0].ParentRoleKey = nil },
	}
	for name, mutate := range mutations {
		t.Run(name, func(t *testing.T) {
			changed := validView()
			mutate(&changed)
			digest, err := ViewDigest(changed)
			if err != nil {
				t.Fatal(err)
			}
			if digest == baseDigest {
				t.Fatal("semantic change did not change view digest")
			}
		})
	}
}

func TestValidateViewHandlesDeepRoleForestIteratively(t *testing.T) {
	const depth = 20_000
	roles := make([]RoleIdentity, depth)
	for index := range roles {
		key := RoleKey{NamespaceID: "tenant.example", LocalKey: fmt.Sprintf("role-%05d", index)}
		roles[index] = RoleIdentity{
			Key: key, ResolvedRoleID: fmt.Sprintf("id-%05d", index), State: IdentityStateActive,
		}
		if index > 0 {
			parent := roles[index-1].Key
			roles[index].ParentRoleKey = &parent
		}
	}
	view := View{
		OrgID: "org-deep", CanonicalizationVersion: ViewCanonicalizationVersion,
		KeyGrammarVersion: KeyGrammarVersion, RoleTopologyGrammarVersion: RoleTopologyGrammarVersion,
		Profiles: []ProfileIdentity{}, Roles: roles,
	}
	if err := ValidateView(view); err != nil {
		t.Fatalf("deep forest rejected: %v", err)
	}
	parent := roles[len(roles)-1].Key
	view.Roles[0].ParentRoleKey = &parent
	if err := ValidateView(view); err == nil {
		t.Fatal("deep cycle accepted")
	}
}

func TestBindingAttestsExactViewWithoutCouplingEpochToContent(t *testing.T) {
	view := validView()
	digest, err := ViewDigest(view)
	if err != nil {
		t.Fatal(err)
	}
	for _, epoch := range []uint64{1, ^uint64(0)} {
		binding := Binding{
			OrgID: view.OrgID, SecurityEpoch: epoch,
			LayoutRoutingSecurityViewDigest: digest,
		}
		if err := ValidateBindingForView(binding, view); err != nil {
			t.Fatalf("epoch %d rejected for unchanged content: %v", epoch, err)
		}
	}
	badOrg := Binding{OrgID: "org-2", SecurityEpoch: 1, LayoutRoutingSecurityViewDigest: digest}
	if err := ValidateBindingForView(badOrg, view); err == nil {
		t.Fatal("cross-org attestation accepted")
	}
	badDigest := Binding{OrgID: view.OrgID, SecurityEpoch: 1, LayoutRoutingSecurityViewDigest: testDigestWith("f")}
	if err := ValidateBindingForView(badDigest, view); err == nil {
		t.Fatal("wrong view digest accepted")
	}
}

func testDigestWith(digit string) string {
	return "sha256:" + strings.Repeat(digit, 64)
}

func validView() View {
	manager := RoleKey{NamespaceID: "tenant.example", LocalKey: "manager"}
	return View{
		OrgID:                      "org-1",
		CanonicalizationVersion:    ViewCanonicalizationVersion,
		KeyGrammarVersion:          KeyGrammarVersion,
		RoleTopologyGrammarVersion: RoleTopologyGrammarVersion,
		Profiles: []ProfileIdentity{
			{Key: ProfileKey{NamespaceID: "tenant.example", LocalKey: "support-user"}, ResolvedProfileID: "00e-support", State: IdentityStateActive},
			{Key: ProfileKey{NamespaceID: "io.nexuscrm.system", LocalKey: "standard-user"}, ResolvedProfileID: "00e-standard", State: IdentityStateActive},
		},
		Roles: []RoleIdentity{
			{Key: RoleKey{NamespaceID: "tenant.example", LocalKey: "seller"}, ResolvedRoleID: "role-seller", State: IdentityStateActive, ParentRoleKey: &manager},
			{Key: manager, ResolvedRoleID: "role-manager", State: IdentityStateActive},
			{Key: RoleKey{NamespaceID: "tenant.example", LocalKey: "old-role"}, ResolvedRoleID: "role-old", State: IdentityStateTombstone},
		},
	}
}

func assertStructFields(t *testing.T, typ reflect.Type, want []string) {
	t.Helper()
	if typ.NumField() != len(want) {
		t.Fatalf("%s has %d fields, want %d", typ.Name(), typ.NumField(), len(want))
	}
	for index, name := range want {
		if got := typ.Field(index).Name; got != name {
			t.Fatalf("%s field %d = %q, want %q", typ.Name(), index, got, name)
		}
	}
}
