package layoutroutingsecurityv1

import (
	"bytes"
	"testing"
)

func TestParseCanonicalViewReturnsIndependentCanonicalView(t *testing.T) {
	view := validView()
	raw, err := CanonicalViewBytes(view)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := ParseCanonicalView(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 view")
	}
	var child *RoleIdentity
	var manager *RoleIdentity
	for index := range parsed.Roles {
		if parsed.Roles[index].ParentRoleKey != nil {
			child = &parsed.Roles[index]
		}
		if parsed.Roles[index].Key.LocalKey == "manager" {
			manager = &parsed.Roles[index]
		}
	}
	if child == nil || manager == nil {
		t.Fatal("fixture did not provide an independent parent edge")
	}

	org := parsed.OrgID
	parent := *child.ParentRoleKey
	for index := range raw {
		raw[index] ^= 0xff
	}
	if parsed.OrgID != org {
		t.Fatal("parsed OrgID aliases input bytes")
	}
	if *child.ParentRoleKey != parent {
		t.Fatal("parsed parent key aliases input bytes")
	}
	child.ParentRoleKey.LocalKey = "changed"
	if manager.Key.LocalKey == "changed" {
		t.Fatal("parsed parent key aliases another returned role key")
	}
}

func TestParseCanonicalViewPreservesPresentEmptyCollections(t *testing.T) {
	view := View{
		OrgID:                      "org-empty",
		CanonicalizationVersion:    ViewCanonicalizationVersion,
		KeyGrammarVersion:          KeyGrammarVersion,
		RoleTopologyGrammarVersion: RoleTopologyGrammarVersion,
		Profiles:                   []ProfileIdentity{},
		Roles:                      []RoleIdentity{},
	}
	raw, err := CanonicalViewBytes(view)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := ParseCanonicalView(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 TestParseCanonicalViewRejectsMalformedOrNoncanonicalBytes(t *testing.T) {
	valid, err := CanonicalViewBytes(validView())
	if err != nil {
		t.Fatal(err)
	}

	tests := map[string][]byte{
		"empty":                              nil,
		"domain string truncated":            {byte(len(ViewCanonicalizationVersion))},
		"count truncated":                    canonicalPrefixWithoutProfileCount(),
		"profile entry truncated":            truncatedProfileEntry(),
		"role parent truncated":              truncatedRoleParent(),
		"nonminimal domain length":           nonminimalDomainLength(valid),
		"overflowing unsigned LEB128":        overflowingUnsignedLEB128(),
		"profile count exceeds maximum":      excessiveProfileCount(),
		"unknown state tag":                  invalidProfileState(),
		"unknown parent presence tag":        invalidRoleParentPresence(),
		"unordered otherwise valid profiles": unorderedProfiles(),
		"trailing byte":                      append(append([]byte(nil), valid...), 0),
	}
	for name, raw := range tests {
		t.Run(name, func(t *testing.T) {
			if _, err := ParseCanonicalView(raw); err == nil {
				t.Fatal("accepted malformed or noncanonical bytes")
			}
		})
	}
}

func canonicalPrefixWithoutProfileCount() []byte {
	var out bytes.Buffer
	writeCanonicalString(&out, ViewCanonicalizationVersion)
	writeCanonicalString(&out, "org-1")
	writeCanonicalString(&out, KeyGrammarVersion)
	writeCanonicalString(&out, RoleTopologyGrammarVersion)
	return out.Bytes()
}

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

func truncatedRoleParent() []byte {
	var out bytes.Buffer
	out.Write(canonicalPrefixWithoutProfileCount())
	writeCanonicalUint(&out, 0)
	writeCanonicalUint(&out, 1)
	writeCanonicalString(&out, "tenant.example")
	writeCanonicalString(&out, "seller")
	writeCanonicalString(&out, "role-seller")
	out.WriteByte(0x01)
	out.WriteByte(0x01)
	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())
	writeCanonicalUint(&out, MaxProfileIdentities+1)
	return out.Bytes()
}

func invalidProfileState() []byte {
	var out bytes.Buffer
	out.Write(canonicalPrefixWithoutProfileCount())
	writeCanonicalUint(&out, 1)
	writeCanonicalString(&out, "tenant.example")
	writeCanonicalString(&out, "seller")
	writeCanonicalString(&out, "profile-seller")
	out.WriteByte(0x03)
	return out.Bytes()
}

func invalidRoleParentPresence() []byte {
	var out bytes.Buffer
	out.Write(canonicalPrefixWithoutProfileCount())
	writeCanonicalUint(&out, 0)
	writeCanonicalUint(&out, 1)
	writeCanonicalString(&out, "tenant.example")
	writeCanonicalString(&out, "seller")
	writeCanonicalString(&out, "role-seller")
	out.WriteByte(0x01)
	out.WriteByte(0x02)
	return out.Bytes()
}

func unorderedProfiles() []byte {
	view := View{
		OrgID:                      "org-unordered",
		CanonicalizationVersion:    ViewCanonicalizationVersion,
		KeyGrammarVersion:          KeyGrammarVersion,
		RoleTopologyGrammarVersion: RoleTopologyGrammarVersion,
		Profiles: []ProfileIdentity{
			{Key: ProfileKey{NamespaceID: "tenant.example", LocalKey: "bravo"}, ResolvedProfileID: "profile-bravo", State: IdentityStateActive},
			{Key: ProfileKey{NamespaceID: "tenant.example", LocalKey: "alpha"}, ResolvedProfileID: "profile-alpha", State: IdentityStateActive},
		},
		Roles: []RoleIdentity{},
	}
	var out bytes.Buffer
	writeCanonicalString(&out, ViewCanonicalizationVersion)
	writeCanonicalString(&out, view.OrgID)
	writeCanonicalString(&out, view.KeyGrammarVersion)
	writeCanonicalString(&out, view.RoleTopologyGrammarVersion)
	writeCanonicalUint(&out, uint64(len(view.Profiles)))
	for _, identity := range view.Profiles {
		writeCanonicalString(&out, identity.Key.NamespaceID)
		writeCanonicalString(&out, identity.Key.LocalKey)
		writeCanonicalString(&out, identity.ResolvedProfileID)
		out.WriteByte(canonicalStateByte(identity.State))
	}
	writeCanonicalUint(&out, 0)
	return out.Bytes()
}
