package records

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	metadataFeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

func TestPermissionSetDeleteOwnedGrantsOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	if err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
		t.Fatalf("delete populated PermissionSet: %v", err)
	}
	assertPermissionSetDeleteState(t, f, true)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT Revision FROM _System_PermissionStateVersion WHERE SingletonKey=1", 3)
	if err := f.records.RestoreRecord(ctx, "0PS-authoring", actor); !isRecycleBinNotFoundError(err) {
		t.Fatalf("permanently deleted definition must not be restorable: %v", err)
	}
}

func TestPermissionSetDeletePurgesPreviouslyDeletedOwnedGrantsOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	for _, target := range [][2]string{{constants.TableObjectPermissions, "op-authoring"}, {constants.TableFieldPermissions, "fp-authoring"}} {
		if err := f.records.Delete(ctx, target[0], target[1], actor); err != nil {
			t.Fatal(err)
		}
	}
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM _System_RecycleBin WHERE RecordId IN ('op-authoring','fp-authoring')", 2)
	// An unrelated definition's deleted grant must keep its row and tombstone.
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO PermissionSet (Id,Name,Label) VALUES ('0PS-unrelated','Unrelated','Unrelated')")
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO FieldPermissions (Id,ParentId,SobjectType,Field) VALUES ('fp-unrelated','0PS-unrelated','Account','Name')")
	if err := f.records.Delete(ctx, constants.TableFieldPermissions, "fp-unrelated", actor); err != nil {
		t.Fatal(err)
	}
	// Aborting the parent delete must restore even already-deleted grant storage
	// and its original recycle-bin entries, not merely the live permissions.
	wantErr := errors.New("abort permanent definition cleanup")
	_, err := f.records.RunRecordMutationTransactionWithOutcome(ctx, []string{constants.TablePermissionSet}, func(txCtx context.Context) error {
		if err := f.records.Delete(txCtx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
			return err
		}
		return wantErr
	})
	if !errors.Is(err, wantErr) {
		t.Fatal(err)
	}
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM _System_RecycleBin WHERE RecordId IN ('op-authoring','fp-authoring')", 2)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM FieldPermissions WHERE Id='fp-authoring' AND IsDeleted=1", 1)
	if err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
		t.Fatal(err)
	}
	assertPermissionSetDeleteState(t, f, true)
	for _, id := range []string{"0PS-authoring", "op-authoring", "fp-authoring"} {
		if err := f.records.RestoreRecord(ctx, id, actor); !isRecycleBinNotFoundError(err) {
			t.Errorf("deleted definition member %s remains restorable: %v", id, err)
		}
	}
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM PermissionSet WHERE Id='0PS-unrelated' AND IsDeleted=0", 1)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM FieldPermissions WHERE Id='fp-unrelated' AND IsDeleted=1", 1)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM _System_RecycleBin WHERE RecordId='fp-unrelated'", 1)
}

func TestPermissionSetDeleteRetiredExternalReferenceIsDomainRejectionOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO PermissionSetAssignment (Id,AssigneeId,PermissionSetId,IsDeleted) VALUES ('retired-assignment','admin-denied','0PS-authoring',1)")
	err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor)
	if !sharedErrors.IsConflict(err) {
		t.Fatalf("historical external reference must not be silently FK-cascaded or become a 500: %v", err)
	}
	assertPermissionSetDeleteState(t, f, false)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM PermissionSetAssignment WHERE Id='retired-assignment' AND IsDeleted=1", 1)
}

func TestPermissionSetDeleteRetiresOwnedVisibilityOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	definitions, err := schemacatalog.SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	for _, definition := range definitions {
		if definition.TableName == constants.TableApp {
			if err := persistence.NewSchemaRepository(f.db, nil).CreatePhysicalTable(ctx, definition); err != nil {
				t.Fatal(err)
			}
		}
	}
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO _System_App (Id,Name,Label,Icon,Color,NavigationItems,UtilityItems,AssignedProfiles) VALUES ('app-delete-test','DeleteTest','Delete test','test','#000000','[]','[]','[]')")
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO PermissionSetTabSetting (Id,PermissionSetId,TabName) VALUES ('tab-delete-test','0PS-authoring','TestTab')")
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO PermissionSetAppVisibility (Id,PermissionSetId,AppId) VALUES ('app-visible-delete-test','0PS-authoring','app-delete-test')")
	if err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
		t.Fatal(err)
	}
	assertPermissionSetDeleteState(t, f, true)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM PermissionSetTabSetting WHERE PermissionSetId='0PS-authoring'", 0)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM PermissionSetAppVisibility WHERE PermissionSetId='0PS-authoring'", 0)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM _System_App WHERE Id='app-delete-test'", 1)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM _System_RecycleBin WHERE RecordId IN ('tab-delete-test','app-visible-delete-test')", 0)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT Revision FROM _System_PermissionStateVersion WHERE SingletonKey=1", 3)
}

func TestPermissionSetDeleteRollsBackWholeDefinitionOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	wantErr := errors.New("abort outer transaction after delete")
	_, err := f.records.RunRecordMutationTransactionWithOutcome(ctx, []string{constants.TablePermissionSet}, func(txCtx context.Context) error {
		if err := f.records.Delete(txCtx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
			return err
		}
		return wantErr
	})
	if !errors.Is(err, wantErr) {
		t.Fatalf("rollback injection: %v", err)
	}
	assertPermissionSetDeleteState(t, f, false)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT Revision FROM _System_PermissionStateVersion WHERE SingletonKey=1", 2)
}

func TestPermissionSetDeleteAssignedSetIsDomainRejectionOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO PermissionSetAssignment (Id,AssigneeId,PermissionSetId) VALUES ('assignment-delete-test','admin-denied','0PS-authoring')")
	err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor)
	if !sharedErrors.IsConflict(err) {
		t.Fatalf("assigned set deletion must be a semantic conflict: %v", err)
	}
	result, recognized := classifyRuntimeMutationDomainError(semantic.Operation{Kind: semantic.OperationDelete, ObjectID: "PermissionSet"}, 0, err)
	if !recognized || len(result.Errors) != 1 || result.Errors[0].Kind != semantic.ErrorInvalidOperation {
		t.Fatalf("delete rejection would become an internal error: %+v, %v", result, recognized)
	}
	assertPermissionSetDeleteState(t, f, false)
	assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM PermissionSetAssignment WHERE Id='assignment-delete-test' AND IsDeleted=0", 1)
}

func TestPermissionSetDeleteManagedSetIsRejectedBeforeCleanupOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	permissionAuthoringAggregateExec(t, f.db, "UPDATE PermissionSet SET OwnerPack='managed-test-pack' WHERE Id='0PS-authoring'")
	if err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor); err == nil {
		t.Fatal("deleted a managed definition")
	}
	assertPermissionSetDeleteState(t, f, false)
}

func TestPermissionSetDeleteHistoricalAliasesAndDisabledGrantsOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO FieldPermissions (Id,ParentId,SobjectType,Field,PermissionsRead) VALUES ('historical-name','0PS-authoring','Account','Name',1)")
	permissionAuthoringAggregateExec(t, f.db, "UPDATE _System_PermissionSetPermission SET IsEnabled=0 WHERE PermissionSetId='0PS-authoring'")
	if err := f.records.Delete(ctx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
		t.Fatal(err)
	}
	assertPermissionSetDeleteState(t, f, true)
}

func TestPermissionSetDeleteSerializesWithAuthoringSaveOnTiDB(t *testing.T) {
	f, ctx, actor := permissionSetDeleteFixture(t)
	written, release := make(chan struct{}), make(chan struct{})
	var once sync.Once
	var workers sync.WaitGroup
	finish := func() { once.Do(func() { close(release) }) }
	defer func() { finish(); workers.Wait() }()
	deleteDone := make(chan error, 1)
	workers.Add(1)
	go func() {
		defer workers.Done()
		_, err := f.records.RunRecordMutationTransactionWithOutcome(ctx, []string{constants.TablePermissionSet}, func(txCtx context.Context) error {
			if err := f.records.Delete(txCtx, constants.TablePermissionSet, "0PS-authoring", actor); err != nil {
				return err
			}
			close(written)
			select {
			case <-release:
				return nil
			case <-txCtx.Done():
				return txCtx.Err()
			}
		})
		deleteDone <- err
	}()
	select {
	case <-written:
	case err := <-deleteDone:
		t.Fatalf("delete before barrier: %v", err)
	case <-ctx.Done():
		t.Fatal(ctx.Err())
	}
	reached := make(chan struct{}, 1)
	service, err := permissions.NewPermissionAuthoringService(f.txm, f.commands,
		permissionAuthoringCompatibilityLockSignal{PermissionAuthoringTargetStore: f.targets, reached: reached},
		f.policy, f.planner, f.audit, f.authority, f.convergence, permissionAuthoringAggregateReceipts{}, permissions.PermissionAuthoringServiceOptions{})
	if err != nil {
		t.Fatal(err)
	}
	invocation := f.invocation(t, "admin-1")
	request := permissionAuthoringAggregateRequest(t, "save-during-delete")
	request.ExpectedAuthoringToken = permissionAuthoringTokenForTest(t, "0PS-authoring", 2)
	result := make(chan permissionauthoringv1.Response, 1)
	saveErr := make(chan error, 1)
	workers.Add(1)
	go func() {
		defer workers.Done()
		response, err := service.Save(f.ctx, invocation, request)
		result <- response
		saveErr <- err
	}()
	select {
	case <-reached:
	case err := <-saveErr:
		t.Fatalf("save before root lock: %v", err)
	case <-ctx.Done():
		t.Fatal(ctx.Err())
	}
	finish()
	if err := <-deleteDone; err != nil {
		t.Fatal(err)
	}
	response := <-result
	if err := <-saveErr; err != nil {
		t.Fatal(err)
	}
	if response.Outcome != permissionauthoringv1.OutcomeRejected {
		t.Fatalf("save resurrected deleted definition: %+v", response)
	}
	assertPermissionSetDeleteState(t, f, true)
}

func permissionSetDeleteFixture(t *testing.T) (*permissionAuthoringAggregateFixture, context.Context, *models.UserSession) {
	t.Helper()
	f, _, actor := permissionAuthoringCompatibilityFixture(t)
	ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
	t.Cleanup(cancel)
	f.ctx = constants.WithSecurityContext(ctx, constants.UserContext, "permission definition deletion integration")
	ctx = permissions.WithEffectivePermissions(f.ctx, actor.ID, actor.ProfileID, map[string]bool{"ManageProfilesPermSets": true, "ModifyAllData": true}, nil)
	if _, err := f.service(t, f.audit).Save(f.ctx, f.invocation(t, "admin-1"), permissionAuthoringAggregateRequest(t, "populate-before-delete")); err != nil {
		t.Fatal(err)
	}
	definitions, err := schemacatalog.SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	inventory, err := schemacatalog.ManagedPermissionSetDefinitionInventory()
	if err != nil {
		t.Fatal(err)
	}
	schemas := permissionAuthoringAggregateSchemas(t)
	repository := persistence.NewSchemaRepository(f.db, nil)
	for _, definition := range definitions {
		if _, found := inventory.RelationForTable(definition.TableName); !found {
			continue
		}
		// Reuse canonical DDL and access policies, including all reference edges.
		if err := repository.CreatePhysicalTable(ctx, definition); err != nil {
			t.Fatal(err)
		}
		fields := make([]models.FieldMetadata, 0, len(definition.Columns))
		for _, column := range definition.Columns {
			kind := constants.FieldTypeString
			if strings.HasPrefix(strings.ToUpper(column.Type), "TINYINT") {
				kind = constants.FieldTypeBoolean
			}
			fields = append(fields, models.FieldMetadata{APIName: column.Name, Type: kind})
		}
		canonical := permissionAuthoringAggregateSchema(t, definition.TableName, fields)
		canonical.DeleteMode = definition.DeleteMode
		replaced := false
		for _, existing := range schemas {
			if existing.APIName == canonical.APIName {
				replaced = true
				break
			}
		}
		if !replaced {
			schemas = append(schemas, canonical)
		}
	}
	metadata := metadataFeature.NewMetadataService(&permissionAuthoringAggregateMetadataRepo{schemas: schemas}, nil, nil)
	f.records.metadata = metadata
	f.records.repo.(*persistence.RecordRepository).SetSchemaProvider(metadata)
	assertPermissionSetDeleteState(t, f, false)
	return f, ctx, actor
}

func assertPermissionSetDeleteState(t *testing.T, f *permissionAuthoringAggregateFixture, deleted bool) {
	t.Helper()
	live := 1
	if deleted {
		live = 0
	}
	for _, query := range []string{
		"SELECT COUNT(*) FROM PermissionSet WHERE Id='0PS-authoring'",
		"SELECT COUNT(*) FROM ObjectPermissions WHERE ParentId='0PS-authoring'",
		"SELECT COUNT(*) FROM FieldPermissions WHERE ParentId='0PS-authoring'",
		"SELECT COUNT(*) FROM _System_PermissionSetPermission WHERE PermissionSetId='0PS-authoring'",
		"SELECT COUNT(*) FROM PermissionSetCustomPermission WHERE PermissionSetId='0PS-authoring'",
		"SELECT COUNT(*) FROM PermissionSetRecordTypeVisibility WHERE PermissionSetId='0PS-authoring'",
	} {
		assertPermissionAuthoringAggregateCount(t, f.db, query, live)
	}
	if deleted {
		assertPermissionAuthoringAggregateCount(t, f.db, "SELECT COUNT(*) FROM _System_RecycleBin WHERE RecordId IN ('0PS-authoring','op-authoring','fp-authoring','historical-name')", 0)
	}
}
