package schemacatalog

import (
	"strings"
	"testing"
)

func TestManagedAssignmentPolicyStoreInventoryIsCompleteAndImmutable(t *testing.T) {
	inventory, err := ManagedAssignmentPolicyStoreInventory()
	if err != nil {
		t.Fatal(err)
	}
	if inventory.SchemaVersion() != AssignmentPolicyStoreInventorySchema {
		t.Fatalf("schema version = %q", inventory.SchemaVersion())
	}
	stores := inventory.Stores()
	if len(stores) != 5 {
		t.Fatalf("stores = %#v, want claim, profile selector, release receipt, target state, and tombstone receipt", stores)
	}
	gotRoles := make([]string, len(stores))
	for index, store := range stores {
		gotRoles[index] = store.Role
	}
	if strings.Join(gotRoles, ",") != "claim,profile_selector,release_receipt,target_state,tombstone_receipt" {
		t.Fatalf("roles = %v", gotRoles)
	}
	if paths := inventory.AccessPaths(); len(paths) != 16 {
		t.Fatalf("access paths = %#v, want sixteen reviewed paths", paths)
	}
	stores[0].TableName = "changed"
	stores[0].Columns[0] = "changed"
	again := inventory.Stores()
	if again[0].TableName != AssignmentPolicyClaimTable || again[0].Columns[0] == "changed" {
		t.Fatalf("caller mutation escaped inventory copy: %#v", again)
	}
	canonical := inventory.CanonicalJSON()
	canonical[0] = '!'
	if !strings.HasPrefix(string(inventory.CanonicalJSON()), `{"schemaVersion"`) {
		t.Fatal("caller mutation escaped canonical inventory copy")
	}
	if got := inventory.Digest(); got != "sha256:bfaf68c972962b3f1a07ab6f7de3c4725d6833b5e377774adfd72a22cb496ccd" {
		t.Fatalf("digest = %q; freeze the reviewed assignment-policy store inventory", got)
	}
}

func TestAssignmentPolicyClaimIsDefinitionNegativeEdge(t *testing.T) {
	inventory, err := ManagedPermissionSetDefinitionInventory()
	if err != nil {
		t.Fatal(err)
	}
	relation, ok := inventory.RelationForTable(AssignmentPolicyClaimTable)
	if !ok || relation.Role != PermissionSetDefinitionRoleNegativeEdge || relation.ParentField != "PermissionSetId" {
		t.Fatalf("claim relation = %#v, found %t", relation, ok)
	}
	if _, exists := inventory.RelationForTable(AssignmentPolicySelectorTable); exists {
		t.Fatal("profile selectors unexpectedly joined the PermissionSet lifecycle closure")
	}
	if _, exists := inventory.RelationForTable(AssignmentPolicyTargetStateTable); exists {
		t.Fatal("durable target state unexpectedly joined the PermissionSet lifecycle closure")
	}
}

func TestAssignmentPolicyTargetStateIsPerDefinitionWhileAssignmentsFanOut(t *testing.T) {
	inventory, err := ManagedAssignmentPolicyStoreInventory()
	if err != nil {
		t.Fatal(err)
	}
	paths := make(map[string]AssignmentPolicyAccessPath)
	for _, path := range inventory.AccessPaths() {
		paths[path.IndexName] = path
	}
	definition := paths["uq_ps_assignment_policy_definition"]
	if definition.TableName != AssignmentPolicyTargetStateTable ||
		strings.Join(definition.Columns, ",") != "PermissionSetId" ||
		!definition.Unique {
		t.Fatalf("definition aggregate path = %#v", definition)
	}
	assignments := paths["idx_psa_permset_assignee"]
	if assignments.TableName != "PermissionSetAssignment" ||
		strings.Join(assignments.Columns, ",") != "PermissionSetId,AssigneeId" ||
		assignments.Unique {
		t.Fatalf("assignment fan-out path = %#v", assignments)
	}
}

func TestAssignmentPolicyTargetStateUsesPortableUint64Generations(t *testing.T) {
	definitions, err := SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	state := findSystemTableDefinition(t, definitions, AssignmentPolicyTargetStateTable)
	columns := columnDefinitionsByName(state)
	requireColumn(t, columns, "WorkGeneration", "DECIMAL(20,0)", false, false, "0")
	requireColumn(t, columns, "AppliedGeneration", "DECIMAL(20,0)", false, false, "0")
}

func TestAssignmentPolicyRemovalEvidenceAndSuspensionStateAreIndependent(t *testing.T) {
	inventory, err := ManagedAssignmentPolicyStoreInventory()
	if err != nil {
		t.Fatal(err)
	}
	stores := make(map[string]AssignmentPolicyStore)
	for _, store := range inventory.Stores() {
		stores[store.Role] = store
	}
	release := stores[AssignmentPolicyStoreRoleReleaseReceipt]
	if release.TableName != AssignmentPolicyReleaseReceiptTable {
		t.Fatalf("release receipt store = %#v", release)
	}
	state := stores[AssignmentPolicyStoreRoleTargetState]
	if state.TableName != AssignmentPolicyTargetStateTable ||
		!containsExactAssignmentPolicyColumn(state.Columns, "Suspended") ||
		!containsExactAssignmentPolicyColumn(state.Columns, "ResolutionState") {
		t.Fatalf("target state store = %#v", state)
	}
	if release.TableName == state.TableName {
		t.Fatal("policy release evidence and target-scoped suspension share one lifecycle row")
	}
}

func TestAssignmentPolicyReleaseReceiptIdentityIsPerSourceAndTarget(t *testing.T) {
	inventory, err := ManagedAssignmentPolicyStoreInventory()
	if err != nil {
		t.Fatal(err)
	}
	stores := make(map[string]AssignmentPolicyStore)
	for _, store := range inventory.Stores() {
		stores[store.Role] = store
	}
	release := stores[AssignmentPolicyStoreRoleReleaseReceipt]
	if release.TableName != "_System_PermissionSetAssignmentPolicyTargetReleaseReceipt" {
		t.Fatalf("authoritative release receipt table = %q", release.TableName)
	}

	paths := make(map[string]AssignmentPolicyAccessPath)
	for _, path := range inventory.AccessPaths() {
		paths[path.IndexName] = path
	}
	identity := paths["uq_ps_assignment_policy_release_source_target"]
	if identity.TableName != release.TableName ||
		strings.Join(identity.Columns, ",") != "OwnerPack,PolicyKey,PermissionSetId" ||
		!identity.Unique {
		t.Fatalf("release receipt identity path = %#v", identity)
	}
	if _, sourceOnly := paths["uq_ps_assignment_policy_release_source"]; sourceOnly {
		t.Fatal("source-only release receipt identity remains authoritative")
	}

	definitions, err := SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	definition := findSystemTableDefinition(t, definitions, release.TableName)
	columns := columnDefinitionsByName(definition)
	worstCaseBytes := 0
	for _, columnName := range identity.Columns {
		columnBytes, widthErr := maxTiDBIndexBytes(columns[columnName].Type)
		if widthErr != nil {
			t.Fatalf("release receipt identity column %s: %v", columnName, widthErr)
		}
		worstCaseBytes += columnBytes
	}
	if worstCaseBytes > tidbMaxIndexKeyBytes {
		t.Fatalf("release receipt identity key bytes = %d, want <= %d", worstCaseBytes, tidbMaxIndexKeyBytes)
	}
}

func TestAssignmentPolicyTombstoneReceiptBindsExactAssignmentPair(t *testing.T) {
	inventory, err := ManagedAssignmentPolicyStoreInventory()
	if err != nil {
		t.Fatal(err)
	}
	paths := make(map[string]AssignmentPolicyAccessPath)
	for _, path := range inventory.AccessPaths() {
		paths[path.IndexName] = path
	}
	assignment := paths["uq_ps_assignment_policy_tombstone_assignment"]
	if assignment.TableName != AssignmentPolicyTombstoneReceiptTable ||
		strings.Join(assignment.Columns, ",") != "AssignmentId" || !assignment.Unique {
		t.Fatalf("assignment tombstone identity path = %#v", assignment)
	}
	pair := paths["uq_ps_assignment_policy_tombstone_pair"]
	if pair.TableName != AssignmentPolicyTombstoneReceiptTable ||
		strings.Join(pair.Columns, ",") != "PermissionSetId,AssigneeId" || !pair.Unique {
		t.Fatalf("assignment tombstone pair path = %#v", pair)
	}

	definitions, err := SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	tombstone := findSystemTableDefinition(t, definitions, AssignmentPolicyTombstoneReceiptTable)
	columns := columnDefinitionsByName(tombstone)
	requireColumn(t, columns, "TombstonedGeneration", "DECIMAL(20,0)", false, false, "0")
}

func containsExactAssignmentPolicyColumn(columns []string, want string) bool {
	for _, column := range columns {
		if column == want {
			return true
		}
	}
	return false
}
