package layoutroutingidentitymappingv1

import (
	"fmt"
	"reflect"
	"testing"

	routing "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutroutingsecurityv1"
)

func TestProposalOwnsOnlyTypedMappingContent(t *testing.T) {
	assertFields(t, reflect.TypeOf(ProfileMapping{}), []string{"Key", "ResolvedProfileID"})
	assertFields(t, reflect.TypeOf(RoleMapping{}), []string{"Key", "ResolvedRoleID"})
	assertFields(t, reflect.TypeOf(Proposal{}), []string{
		"OrgID", "CanonicalizationVersion", "KeyGrammarVersion", "Profiles", "Roles",
	})
}

func TestValidateProposalAcceptsTypedDimensionSpecificLocalIDs(t *testing.T) {
	proposal := validProposal()
	proposal.Profiles[0].ResolvedProfileID = proposal.Roles[0].ResolvedRoleID
	if err := ValidateProposal(proposal); err != nil {
		t.Fatalf("same local ID bytes across typed dimensions rejected: %v", err)
	}
}

func TestValidateProposalRejectsIncompleteOrAmbiguousContent(t *testing.T) {
	tests := map[string]func(*Proposal){
		"blank org": func(value *Proposal) { value.OrgID = "" },
		"unsupported canonicalization": func(value *Proposal) {
			value.CanonicalizationVersion = "v2"
		},
		"unsupported key grammar": func(value *Proposal) { value.KeyGrammarVersion = "v2" },
		"missing profiles":        func(value *Proposal) { value.Profiles = nil },
		"missing roles":           func(value *Proposal) { value.Roles = nil },
		"invalid profile key": func(value *Proposal) {
			value.Profiles[0].Key.LocalKey = "Profile"
		},
		"invalid profile id": func(value *Proposal) {
			value.Profiles[0].ResolvedProfileID = " profile"
		},
		"duplicate profile key": func(value *Proposal) {
			value.Profiles[1].Key = value.Profiles[0].Key
		},
		"duplicate profile id": func(value *Proposal) {
			value.Profiles[1].ResolvedProfileID = value.Profiles[0].ResolvedProfileID
		},
		"invalid role key": func(value *Proposal) {
			value.Roles[0].Key.NamespaceID = "Tenant.example"
		},
		"invalid role id": func(value *Proposal) {
			value.Roles[0].ResolvedRoleID = "role\n1"
		},
		"duplicate role key": func(value *Proposal) {
			value.Roles[1].Key = value.Roles[0].Key
		},
		"duplicate role id": func(value *Proposal) {
			value.Roles[1].ResolvedRoleID = value.Roles[0].ResolvedRoleID
		},
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			proposal := validProposal()
			mutate(&proposal)
			if err := ValidateProposal(proposal); err == nil {
				t.Fatal("invalid proposal accepted")
			}
			if canonical, err := CanonicalProposalBytes(proposal); err == nil || canonical != nil {
				t.Fatalf("invalid proposal acquired canonical bytes: %x, err=%v", canonical, err)
			}
			if digest, err := ProposalDigest(proposal); err == nil || digest != "" {
				t.Fatalf("invalid proposal acquired digest %q, err=%v", digest, err)
			}
		})
	}
}

func TestValidateProposalRejectsCountOverflowBeforeEntries(t *testing.T) {
	proposal := validProposal()
	proposal.Profiles = make([]ProfileMapping, MaxProfileMappings+1)
	if err := ValidateProposal(proposal); err == nil {
		t.Fatal("Profile count overflow accepted")
	}
	proposal = validProposal()
	proposal.Roles = make([]RoleMapping, MaxRoleMappings+1)
	if err := ValidateProposal(proposal); err == nil {
		t.Fatal("Role count overflow accepted")
	}
}

func TestCanonicalizeProposalIsPermutationInvariantAndDefensive(t *testing.T) {
	proposal := validProposal()
	digest, err := ProposalDigest(proposal)
	if err != nil {
		t.Fatal(err)
	}
	proposal.Profiles[0], proposal.Profiles[1] = proposal.Profiles[1], proposal.Profiles[0]
	proposal.Roles[0], proposal.Roles[1] = proposal.Roles[1], proposal.Roles[0]
	permutedDigest, err := ProposalDigest(proposal)
	if err != nil {
		t.Fatal(err)
	}
	if permutedDigest != digest {
		t.Fatalf("permutation changed digest: %s != %s", permutedDigest, digest)
	}
	canonical, err := CanonicalizeProposal(proposal)
	if err != nil {
		t.Fatal(err)
	}
	for index := 1; index < len(canonical.Profiles); index++ {
		if !profileMappingLess(canonical.Profiles[index-1], canonical.Profiles[index]) {
			t.Fatal("canonical Profiles are not strictly sorted")
		}
	}
	for index := 1; index < len(canonical.Roles); index++ {
		if !roleMappingLess(canonical.Roles[index-1], canonical.Roles[index]) {
			t.Fatal("canonical Roles are not strictly sorted")
		}
	}
	proposal.Profiles[0].Key.LocalKey = "changed"
	if canonical.Profiles[0].Key.LocalKey == "changed" {
		t.Fatal("canonical Profiles alias caller storage")
	}
}

func TestCanonicalizeProposalPreservesPresentEmptyDimensions(t *testing.T) {
	proposal := Proposal{
		OrgID:                   "org-empty",
		CanonicalizationVersion: CanonicalizationVersion,
		KeyGrammarVersion:       routing.KeyGrammarVersion,
		Profiles:                []ProfileMapping{},
		Roles:                   []RoleMapping{},
	}
	canonical, err := CanonicalizeProposal(proposal)
	if err != nil {
		t.Fatal(err)
	}
	if canonical.Profiles == nil || canonical.Roles == nil {
		t.Fatalf("canonical empty dimensions = %#v / %#v, want present empty slices", canonical.Profiles, canonical.Roles)
	}
	if err := ValidateProposal(canonical); err != nil {
		t.Fatalf("canonical empty proposal is invalid: %v", err)
	}
	if _, err := ProposalDigest(canonical); err != nil {
		t.Fatalf("canonical empty proposal has no digest: %v", err)
	}
}

func TestProposalDigestCommitsEveryMappingContentClass(t *testing.T) {
	baseline := validProposal()
	baseDigest, err := ProposalDigest(baseline)
	if err != nil {
		t.Fatal(err)
	}
	mutations := map[string]func(*Proposal){
		"org":         func(value *Proposal) { value.OrgID = "org-2" },
		"profile key": func(value *Proposal) { value.Profiles[0].Key.LocalKey = "support-admin" },
		"profile id":  func(value *Proposal) { value.Profiles[0].ResolvedProfileID = "profile-other" },
		"role key":    func(value *Proposal) { value.Roles[0].Key.LocalKey = "seller-admin" },
		"role id":     func(value *Proposal) { value.Roles[0].ResolvedRoleID = "role-other" },
	}
	for name, mutate := range mutations {
		t.Run(name, func(t *testing.T) {
			changed := validProposal()
			mutate(&changed)
			digest, err := ProposalDigest(changed)
			if err != nil {
				t.Fatal(err)
			}
			if digest == baseDigest {
				t.Fatal("semantic change did not change proposal digest")
			}
		})
	}
}

func validProposal() Proposal {
	return Proposal{
		OrgID:                   "org-test",
		CanonicalizationVersion: CanonicalizationVersion,
		KeyGrammarVersion:       routing.KeyGrammarVersion,
		Profiles: []ProfileMapping{
			{Key: routing.ProfileKey{NamespaceID: "tenant.example", LocalKey: "support"}, ResolvedProfileID: "profile-support"},
			{Key: routing.ProfileKey{NamespaceID: "tenant.example", LocalKey: "seller"}, ResolvedProfileID: "profile-seller"},
		},
		Roles: []RoleMapping{
			{Key: routing.RoleKey{NamespaceID: "tenant.example", LocalKey: "manager"}, ResolvedRoleID: "role-manager"},
			{Key: routing.RoleKey{NamespaceID: "tenant.example", LocalKey: "seller"}, ResolvedRoleID: "role-seller"},
		},
	}
}

func assertFields(t *testing.T, typ reflect.Type, want []string) {
	t.Helper()
	got := make([]string, typ.NumField())
	for index := range got {
		got[index] = typ.Field(index).Name
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("%s fields = %v, want %v", typ.Name(), got, want)
	}
}

func ExampleProposal() {
	proposal := Proposal{
		OrgID:                   "org-example",
		CanonicalizationVersion: CanonicalizationVersion,
		KeyGrammarVersion:       routing.KeyGrammarVersion,
		Profiles: []ProfileMapping{{
			Key:               routing.ProfileKey{NamespaceID: "tenant.example", LocalKey: "support"},
			ResolvedProfileID: "profile-support",
		}},
		Roles: []RoleMapping{},
	}
	digest, _ := ProposalDigest(proposal)
	fmt.Println(len(digest) == len("sha256:")+64)
	// Output: true
}
