package records

import (
	"context"
	"errors"
	"reflect"
	"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"
)

func TestProfileDeleteRetainsConfigurationAndRestoresCarrierOnTiDB(t *testing.T) {
	f, ctx, actor, id := profileDeleteFixture(t)
	var carrier string
	if err := f.db.QueryRow("SELECT Id FROM PermissionSet WHERE ProfileId=? AND IsOwnedByProfile=1 AND IsDeleted=0", id).Scan(&carrier); err != nil {
		t.Fatal(err)
	}
	if _, err := f.db.Exec("UPDATE ProfileAppVisibility SET IsVisible=1, IsDefault=1 WHERE ProfileId=?", id); err != nil {
		t.Fatal(err)
	}
	if _, err := f.db.Exec("INSERT INTO ProfileTabSetting (Id,ProfileId,TabName,Visibility) VALUES ('retained-tab',?,'Account','DefaultOn')", id); err != nil {
		t.Fatal(err)
	}
	if _, err := f.db.Exec("INSERT INTO ProfileCustomPermission (Id,ProfileId,CustomPermissionId) VALUES ('retained-custom',?,'custom-export')", id); err != nil {
		t.Fatal(err)
	}
	if _, err := f.db.Exec("UPDATE ObjectPermissions SET PermissionsRead=1 WHERE ParentId=? AND SobjectType='Account'", carrier); err != nil {
		t.Fatal(err)
	}
	reader, err := permissions.NewPermissionAuthoringSnapshotService(f.txm, f.targets, f.policy, permissionAuthoringAggregateReceipts{})
	if err != nil {
		t.Fatal(err)
	}
	read := func() (permissionauthoringv1.SnapshotResponse, error) {
		return reader.Read(ctx, f.invocation(t, "admin-1"), permissionauthoringv1.SnapshotRequest{Target: permissionauthoringv1.Target{Kind: permissionauthoringv1.TargetProfile, ID: id}})
	}
	before, err := read()
	if err != nil {
		t.Fatal(err)
	}
	rollback := errors.New("abort after Profile lifecycle")
	_, err = f.records.RunRecordMutationTransactionWithOutcome(ctx, []string{constants.TableProfile}, func(txCtx context.Context) error {
		if err := f.records.Delete(txCtx, constants.TableProfile, id, actor); err != nil {
			return err
		}
		return rollback
	})
	if !errors.Is(err, rollback) {
		t.Fatalf("delete rollback: %v", err)
	}
	rolledBack, err := read()
	if err != nil || rolledBack.CanonicalPermissionSetID != carrier || !reflect.DeepEqual(rolledBack.Grants, before.Grants) {
		t.Fatalf("rollback changed Profile configuration: %+v %v", rolledBack, err)
	}
	if err := f.records.Delete(ctx, constants.TableProfile, id, actor); err != nil {
		t.Fatalf("delete newly created Profile with its defaults: %v", err)
	}
	var deleted int
	if err := f.db.QueryRow("SELECT IsDeleted FROM PermissionSet WHERE Id=?", carrier).Scan(&deleted); err != nil || deleted != 1 {
		t.Fatalf("carrier still active: %d, %v", deleted, err)
	}
	if _, err := read(); err == nil {
		t.Fatal("deleted Profile remains authorable")
	}
	apps, err := f.targets.GetEffectiveAppVisibilities(ctx, "unassigned", id)
	if err != nil || len(apps) != 0 {
		t.Fatalf("deleted Profile app grants: %v %v", apps, err)
	}
	tabs, err := f.targets.GetEffectiveTabSettings(ctx, "unassigned", id)
	if err != nil || len(tabs) != 0 {
		t.Fatalf("deleted Profile tab grants: %v %v", tabs, err)
	}
	if err := f.records.RestoreRecord(ctx, id, actor); err != nil {
		t.Fatalf("restore Profile: %v", err)
	}
	after, err := read()
	if err != nil {
		t.Fatalf("read restored Profile: %v", err)
	}
	if after.CanonicalPermissionSetID != carrier {
		t.Fatalf("restore replaced carrier: %s != %s", after.CanonicalPermissionSetID, carrier)
	}
	if !reflect.DeepEqual(after.Grants, before.Grants) {
		t.Fatalf("restore lost grants: before=%+v after=%+v", before.Grants, after.Grants)
	}
	apps, err = f.targets.GetEffectiveAppVisibilities(ctx, "unassigned", id)
	if err != nil || !apps["app-profile-test"].IsVisible || !apps["app-profile-test"].IsDefault {
		t.Fatalf("restore lost navigation settings: %v %v", apps, err)
	}
	tabs, err = f.targets.GetEffectiveTabSettings(ctx, "unassigned", id)
	if err != nil || tabs["Account"].Visibility != "DefaultOn" {
		t.Fatalf("restore lost tab settings: %v %v", tabs, err)
	}
}

func TestProfileDeleteStillRestrictsExternalUserOnTiDB(t *testing.T) {
	f, ctx, actor, id := profileDeleteFixture(t)
	if _, err := f.db.Exec("UPDATE User SET ProfileId=? WHERE Id='sales-user'", id); err != nil {
		t.Fatal(err)
	}
	if err := f.records.Delete(ctx, constants.TableProfile, id, actor); !sharedErrors.IsConflict(err) {
		t.Fatalf("expected reference conflict, got %v", err)
	}
	var active int
	if err := f.db.QueryRow("SELECT COUNT(*) FROM Profile p JOIN PermissionSet ps ON ps.ProfileId=p.Id WHERE p.Id=? AND p.IsDeleted=0 AND ps.IsDeleted=0", id).Scan(&active); err != nil || active != 1 {
		t.Fatalf("rejected delete changed live state: %d %v", active, err)
	}
}

func TestProfileDeleteSerializesWithAuthoringSaveOnTiDB(t *testing.T) {
	f, ctx, actor, id := profileDeleteFixture(t)
	reader, err := permissions.NewPermissionAuthoringSnapshotService(f.txm, f.targets, f.policy, permissionAuthoringAggregateReceipts{})
	if err != nil {
		t.Fatal(err)
	}
	target := permissionauthoringv1.Target{Kind: permissionauthoringv1.TargetProfile, ID: id}
	snapshot, err := reader.Read(ctx, f.invocation(t, "admin-1"), permissionauthoringv1.SnapshotRequest{Target: target})
	if err != nil {
		t.Fatal(err)
	}
	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() }()
	deleted := make(chan error, 1)
	workers.Add(1)
	go func() {
		defer workers.Done()
		_, err := f.records.RunRecordMutationTransactionWithOutcome(ctx, []string{constants.TableProfile}, func(txCtx context.Context) error {
			if err := f.records.Delete(txCtx, constants.TableProfile, id, actor); err != nil {
				return err
			}
			close(written)
			select {
			case <-release:
				return nil
			case <-txCtx.Done():
				return txCtx.Err()
			}
		})
		deleted <- err
	}()
	select {
	case <-written:
	case err := <-deleted:
		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)
	}
	request := permissionAuthoringAggregateRequest(t, "save-during-profile-delete")
	request.Target = target
	request.ExpectedAuthoringToken = snapshot.AuthoringToken
	request.Changes = permissionauthoringv1.Changes{Fields: []permissionauthoringv1.FieldGrantChange{{ResourceType: "Account", FieldName: "Name", Read: true}}}
	response := make(chan permissionauthoringv1.Response, 1)
	saved := make(chan error, 1)
	invocation := f.invocation(t, "admin-1")
	workers.Add(1)
	go func() {
		defer workers.Done()
		result, err := service.Save(ctx, invocation, request)
		response <- result
		saved <- err
	}()
	select {
	case <-reached:
	case err := <-saved:
		t.Fatalf("save before target resolution: %v", err)
	case <-ctx.Done():
		t.Fatal(ctx.Err())
	}
	finish()
	if err := <-deleted; err != nil {
		t.Fatal(err)
	}
	result := <-response
	if err := <-saved; err != nil {
		t.Fatal(err)
	}
	if result.Outcome != permissionauthoringv1.OutcomeRejected {
		t.Fatalf("Save resurrected deleted Profile: %+v", result)
	}
	var live int
	if err := f.db.QueryRow("SELECT COUNT(*) FROM PermissionSet WHERE Id=? AND IsDeleted=0", snapshot.CanonicalPermissionSetID).Scan(&live); err != nil || live != 0 {
		t.Fatalf("carrier after concurrent Save: %d %v", live, err)
	}
}

func profileDeleteFixture(t *testing.T) (*permissionAuthoringAggregateFixture, context.Context, *models.UserSession, string) {
	t.Helper()
	f, _, actor := permissionAuthoringCompatibilityFixture(t)
	ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
	t.Cleanup(cancel)
	ctx = permissions.WithEffectivePermissions(ctx, actor.ID, actor.ProfileID, map[string]bool{"ManageProfilesPermSets": true, "ModifyAllData": true}, nil)
	definitions, err := schemacatalog.SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	schemas := permissionAuthoringAggregateSchemas(t)
	wanted := map[string]bool{constants.TableProfile: true, constants.TableApp: true, constants.TableProfileAppVisibility: true, constants.TableProfileTabSetting: true, "ProfileLayout": true, "_System_ProfilePermission": true, constants.TableProfileCustomPermission: true, constants.TablePermissionSetAppVisibility: true, constants.TablePermissionSetTabSetting: true}
	repository := persistence.NewSchemaRepository(f.db, nil)
	for _, definition := range definitions {
		if !wanted[definition.TableName] {
			continue
		}
		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, Createable: true, Updateable: true})
		}
		canonical := permissionAuthoringAggregateSchema(t, definition.TableName, fields)
		replaced := false
		for i, existing := range schemas {
			if existing.APIName == canonical.APIName {
				schemas[i] = canonical
				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)
	permissionAuthoringAggregateExec(t, f.db, "INSERT INTO _System_App (Id,Name,Label,Icon,Color,NavigationItems,UtilityItems,AssignedProfiles) VALUES ('app-profile-test','ProfileTest','Profile test','test','#000000','[]','[]','[]')")
	created, err := f.records.Insert(ctx, constants.TableProfile, models.Record{constants.FieldName: "RestoreTest"}, actor)
	if err != nil {
		t.Fatalf("create Profile through Records lifecycle: %v", err)
	}
	id, _ := created[constants.FieldID].(string)
	if id == "" {
		t.Fatal("missing Profile identity")
	}
	return f, ctx, actor, id
}
