package layoutroutingidentitymappingv1

import (
	"bytes"
	"testing"

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

func TestParseCanonicalProposalReturnsIndependentProposal(t *testing.T) {
	proposal := validProposal()
	raw, err := CanonicalProposalBytes(proposal)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := ParseCanonicalProposal(raw)
	if err != nil {
		t.Fatal(err)
	}
	if len(parsed.Profiles) == 0 || len(parsed.Roles) == 0 {
		t.Fatal("fixture did not provide a complete parsed proposal")
	}
	org := parsed.OrgID
	profile := parsed.Profiles[0]
	for index := range raw {
		raw[index] ^= 0xff
	}
	if parsed.OrgID != org || parsed.Profiles[0] != profile {
		t.Fatal("parsed proposal aliases input bytes")
	}
}

func TestParseCanonicalProposalPreservesPresentEmptyCollections(t *testing.T) {
	proposal := Proposal{
		OrgID:                   "org-empty",
		CanonicalizationVersion: CanonicalizationVersion,
		KeyGrammarVersion:       routing.KeyGrammarVersion,
		Profiles:                []ProfileMapping{},
		Roles:                   []RoleMapping{},
	}
	raw, err := CanonicalProposalBytes(proposal)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := ParseCanonicalProposal(raw)
	if err != nil {
		t.Fatal(err)
	}
	if parsed.Profiles == nil || parsed.Roles == nil || len(parsed.Profiles) != 0 || len(parsed.Roles) != 0 {
		t.Fatalf("empty collections = %#v / %#v, want present empty slices", parsed.Profiles, parsed.Roles)
	}
}

func TestParseCanonicalProposalRejectsMalformedOrNoncanonicalBytes(t *testing.T) {
	valid, err := CanonicalProposalBytes(validProposal())
	if err != nil {
		t.Fatal(err)
	}
	tests := map[string][]byte{
		"empty":                          nil,
		"domain string truncated":        {byte(len(CanonicalizationVersion))},
		"count truncated":                canonicalPrefixWithoutProfileCount(),
		"profile entry truncated":        truncatedProfileEntry(),
		"role entry truncated":           truncatedRoleEntry(),
		"nonminimal domain length":       nonminimalDomainLength(valid),
		"overflowing unsigned LEB128":    overflowingUnsignedLEB128(),
		"profile count exceeds maximum":  excessiveProfileCount(),
		"unordered otherwise valid maps": unorderedProfiles(),
		"trailing byte":                  append(append([]byte(nil), valid...), 0),
	}
	for name, raw := range tests {
		t.Run(name, func(t *testing.T) {
			if _, err := ParseCanonicalProposal(raw); err == nil {
				t.Fatal("accepted malformed or noncanonical bytes")
			}
		})
	}
}

func canonicalPrefixWithoutProfileCount() []byte {
	var out bytes.Buffer
	writeString(&out, CanonicalizationVersion)
	writeString(&out, "org-1")
	writeString(&out, routing.KeyGrammarVersion)
	return out.Bytes()
}

func truncatedProfileEntry() []byte {
	var out bytes.Buffer
	out.Write(canonicalPrefixWithoutProfileCount())
	writeUint(&out, 1)
	writeUint(&out, 1)
	return out.Bytes()
}

func truncatedRoleEntry() []byte {
	var out bytes.Buffer
	out.Write(canonicalPrefixWithoutProfileCount())
	writeUint(&out, 0)
	writeUint(&out, 1)
	writeString(&out, "tenant.example")
	writeString(&out, "seller")
	return out.Bytes()
}

func nonminimalDomainLength(valid []byte) []byte {
	result := make([]byte, 0, len(valid)+1)
	result = append(result, valid[0]|0x80, 0x00)
	return append(result, valid[1:]...)
}

func overflowingUnsignedLEB128() []byte {
	return []byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02}
}

func excessiveProfileCount() []byte {
	var out bytes.Buffer
	out.Write(canonicalPrefixWithoutProfileCount())
	writeUint(&out, MaxProfileMappings+1)
	return out.Bytes()
}

func unorderedProfiles() []byte {
	proposal := Proposal{
		OrgID:                   "org-unordered",
		CanonicalizationVersion: CanonicalizationVersion,
		KeyGrammarVersion:       routing.KeyGrammarVersion,
		Profiles: []ProfileMapping{
			{Key: routing.ProfileKey{NamespaceID: "tenant.example", LocalKey: "bravo"}, ResolvedProfileID: "profile-bravo"},
			{Key: routing.ProfileKey{NamespaceID: "tenant.example", LocalKey: "alpha"}, ResolvedProfileID: "profile-alpha"},
		},
		Roles: []RoleMapping{},
	}
	var out bytes.Buffer
	writeString(&out, CanonicalizationVersion)
	writeString(&out, proposal.OrgID)
	writeString(&out, proposal.KeyGrammarVersion)
	writeUint(&out, uint64(len(proposal.Profiles)))
	for _, mapping := range proposal.Profiles {
		writeString(&out, mapping.Key.NamespaceID)
		writeString(&out, mapping.Key.LocalKey)
		writeString(&out, mapping.ResolvedProfileID)
	}
	writeUint(&out, 0)
	return out.Bytes()
}
