package runtimeassembly

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	layoutdocument "github.com/nexuscrm/backend/internal/layoutauthoring"
	database "github.com/nexuscrm/db"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/models"
	authoring "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv2"
)

// Pause only at repository boundaries. Transactions, policy, schema resolution,
// SQL locks, history, rollback and the complete Mutate callback remain real.
type gatedLayoutMutationRepository struct {
	*persistence.MetadataRepository
	firstRoot        func(context.Context) error
	beforeReferences func(context.Context) error
	onRetry          func(context.Context) error
	tx               *sql.Tx
	attempts         atomic.Int32
}

func (r *gatedLayoutMutationRepository) LockLayoutAuthoringAggregate(ctx context.Context, object string) (*models.LayoutAuthoringAggregate, error) {
	tx := r.GetExecutor(ctx).(*sql.Tx)
	first := tx != r.tx
	if first {
		r.tx = tx
		if r.attempts.Add(1) > 1 && r.onRetry != nil {
			if err := r.onRetry(ctx); err != nil {
				return nil, err
			}
		}
	}
	result, err := r.MetadataRepository.LockLayoutAuthoringAggregate(ctx, object)
	if err == nil && first && r.attempts.Load() == 1 && r.firstRoot != nil {
		err = r.firstRoot(ctx)
	}
	return result, err
}
func (r *gatedLayoutMutationRepository) LockLayoutAuthoringSchemaReferences(ctx context.Context, refs []schema.IncarnationReference) error {
	if r.attempts.Load() == 1 && r.beforeReferences != nil {
		if err := r.beforeReferences(ctx); err != nil {
			return err
		}
	}
	return r.MetadataRepository.LockLayoutAuthoringSchemaReferences(ctx, refs)
}

func TestLayoutAuthoringConcurrentCrossObjectSavesOnTiDB(t *testing.T) {
	for _, intervening := range []string{"none", "newer_edit", "deactivated_actor"} {
		t.Run(intervening, func(t *testing.T) {
			ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second)
			defer cancel()
			tables := []string{"_System_Object", "_System_Field", "_System_PageLayout", "_System_LayoutAssignment", "_System_PageLayoutHistory", "_System_PermissionStateVersion", "User", "UserRole", "Profile", "PermissionSet", "PermissionSetAssignment", "_System_Permission", "_System_PermissionSetPermission", "PermissionSetGroupComponent", "PermissionSetGroup", "PermissionSetGroupAssignment", "_System_MutingPermissionSetPermission", "MutingPermissionSet", "_System_CustomPermission", "ProfileCustomPermission", "PermissionSetCustomPermission"}
			db, writer := newLayoutRuntimeTestDB(t, ctx, tables...)
			exec := func(statement string, args ...any) {
				t.Helper()
				if _, err := db.ExecContext(ctx, statement, args...); err != nil {
					t.Fatal(err)
				}
			}
			exec(`INSERT INTO Profile (Id,Name,IsActive) VALUES ('profile-admin','Admin',1)`)
			for _, id := range []string{"a", "b"} {
				exec("INSERT INTO `User` (Id,Username,Email,Password,ProfileId,LastName,IsActive,IsDeleted) VALUES (?,?,?,'test-disabled','profile-admin',?,1,0)", "actor-"+id, "actor-"+id, "actor-"+id+"@example.test", id)
				exec(`INSERT INTO _System_Object (Id,ApiName,TableType,Label,SharingModel) VALUES (?,?,'standard_object',?,'Private')`, "object-"+id, "Work"+strings.ToUpper(id), id)
			}
			exec(`INSERT INTO PermissionSet (Id,Name,Label,ProfileId,IsOwnedByProfile,IsDeleted) VALUES ('grant','Admin','Admin','profile-admin',1,0)`)
			exec(`INSERT INTO _System_Permission (Id,ApiName,Label,Category,IsActive) VALUES ('customize','CustomizeApplication','Customize','system',1)`)
			exec(`INSERT INTO _System_PermissionSetPermission (Id,PermissionSetId,PermissionApiName,IsEnabled) VALUES ('grant-customize','grant','CustomizeApplication',1)`)
			exec(`INSERT INTO _System_Field (Id,ObjectId,ApiName,Label,Type,ReferenceTo) VALUES ('parent','object-b','ParentId','Parent','reference','["WorkA"]')`)
			stateRepo, err := persistence.NewPermissionStateRepository(db)
			if err != nil {
				t.Fatal(err)
			}
			state, err := stateRepo.EnsureInitialized(ctx)
			if err != nil {
				t.Fatal(err)
			}
			aReady, bReady := make(chan struct{}), make(chan struct{})
			repos := make([]*gatedLayoutMutationRepository, 2)
			engines := make([]*LayoutAuthoringEngine, 2)
			requests := make([]semantic.Request, 2)
			for i, id := range []string{"a", "b"} {
				doc := semantic.LayoutDocument{LayoutID: "layout-" + id, ObjectID: "Work" + strings.ToUpper(id), Name: "Original", Kind: "Detail",
					CompactFields: []string{}, Tabs: []string{}, Sections: []authoring.LayoutSection{},
					RelatedLists: []authoring.RelatedList{}, RecordPanels: []authoring.RecordPanel{},
					HeaderActions: authoring.ActionPlacement{Actions: []authoring.LayoutAction{}},
					QuickActions:  authoring.ActionPlacement{Actions: []authoring.LayoutAction{}},
					RecordStates:  []authoring.RecordStateRule{}, PrimaryHeaderActions: []string{},
				}
				if i == 0 {
					doc.RelatedLists = []authoring.RelatedList{{RelatedListID: "children", Label: "Children", ObjectID: "WorkB", LookupFieldID: "ParentId", FieldIDs: []string{}, PrefillMappings: []authoring.PrefillMapping{}}}
				}
				layout, err := layoutdocument.PageLayoutFromDocument(doc)
				if err != nil {
					t.Fatal(err)
				}
				raw, err := json.Marshal(layout)
				if err != nil {
					t.Fatal(err)
				}
				exec(`INSERT INTO _System_PageLayout (Id,ObjectApiName,LayoutName,Config,Source,IsCustomized) VALUES (?,?,'Original',?,'admin_created',1)`, doc.LayoutID, doc.ObjectID, raw)
				pool := db
				if i == 1 {
					pool = writer
				}
				tm := persistence.NewTransactionManager(database.NewConnectionFromDB(pool))
				authority, err := persistence.NewPermissionStateAuthority(tm)
				if err != nil {
					t.Fatal(err)
				}
				repo := &gatedLayoutMutationRepository{MetadataRepository: persistence.NewMetadataRepository(pool)}
				service := metadata.NewMetadataService(repo, nil, nil)
				service.SetTransactionManager(tm)
				perm := permissions.NewPermissionService(persistence.NewPermissionRepository(pool), nil, persistence.NewUserRepository(pool))
				policy, err := permissions.NewTransactionCurrentPermissionAuthoringPolicy(perm, authority)
				if err != nil {
					t.Fatal(err)
				}
				engines[i] = &LayoutAuthoringEngine{metadata: service, permissions: perm, policy: policy, transactions: tm}
				repos[i] = repo
				before, err := engines[i].ReadWorkspace(ctx, doc.ObjectID)
				if err != nil {
					t.Fatal(err)
				}
				changes := preserveLayoutDocumentChanges()
				name := "Saved-" + id
				changes.Name = authoring.Change[string]{Mode: authoring.ChangeSet, Value: &name}
				requests[i] = semantic.Request{ObjectID: doc.ObjectID, ExpectedLayoutRevision: before.Versions.WorkspaceVersion, Operation: semantic.Operation{Update: &semantic.UpdateLayout{LayoutID: doc.LayoutID, Changes: changes, ListFields: semantic.FieldIDListChange{Mode: authoring.ChangePreserve}}}}
				if intervening != "none" {
					layoutID := doc.LayoutID
					repo.onRetry = func(ctx context.Context) error {
						if intervening == "deactivated_actor" {
							_, err := writer.ExecContext(ctx, "UPDATE `User` SET IsActive=0 WHERE Id=?", "actor-"+id)
							return err
						}
						_, err := writer.ExecContext(ctx, `UPDATE _System_PageLayout SET LayoutName='Newer',Config=JSON_SET(Config,'$.layoutName','Newer') WHERE Id=?`, layoutID)
						return err
					}
				}
			}
			repos[0].beforeReferences = func(ctx context.Context) error {
				close(aReady)
				select {
				case <-bReady:
				case <-ctx.Done():
					return ctx.Err()
				}
				for {
					var count int
					if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.DATA_LOCK_WAITS`).Scan(&count); err != nil {
						return err
					}
					if count > 0 {
						return nil
					}
					select {
					case <-time.After(10 * time.Millisecond):
					case <-ctx.Done():
						return ctx.Err()
					}
				}
			}
			repos[1].firstRoot = func(context.Context) error { close(bReady); return nil }
			type outcome struct {
				index    int
				response semantic.Response
				err      error
			}
			results := make(chan outcome, 2)
			run := func(i int) {
				response, err := engines[i].Mutate(ctx, models.UserSession{ID: fmt.Sprintf("actor-%c", 'a'+i), ProfileID: "profile-admin", ProfileName: "Admin"}, state.Version, requests[i])
				results <- outcome{i, response, err}
			}
			go run(0)
			select {
			case <-aReady:
			case result := <-results:
				t.Fatalf("first save before gate: %v", result.err)
			case <-ctx.Done():
				t.Fatal(ctx.Err())
			}
			go run(1)
			for range 2 {
				result := <-results
				retried := repos[result.index].attempts.Load() > 1
				wantName := fmt.Sprintf("Saved-%c", 'a'+result.index)
				wantHistory := 1
				if intervening != "none" && retried {
					if result.err == nil || !reflect.DeepEqual(result.response, semantic.Response{}) {
						t.Fatalf("newer edit overwritten: %#v %v", result.response, result.err)
					}
					if intervening == "newer_edit" {
						var conflictError *apperrors.ConflictError
						if !errors.As(result.err, &conflictError) {
							t.Fatalf("expected revision conflict: %v", result.err)
						}
						wantName = "Newer"
					} else {
						if !strings.Contains(result.err.Error(), "CustomizeApplication") {
							t.Fatalf("expected permission denial: %v", result.err)
						}
						wantName = "Original"
					}
					wantHistory = 0
				} else if result.err != nil || !result.response.Changed {
					t.Fatalf("save %d: %#v %v", result.index, result.response, result.err)
				}
				var name string
				var count int
				if err := db.QueryRowContext(ctx, `SELECT LayoutName FROM _System_PageLayout WHERE Id=?`, requests[result.index].Operation.Update.LayoutID).Scan(&name); err != nil {
					t.Fatal(err)
				}
				if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM _System_PageLayoutHistory WHERE LayoutId=?`, requests[result.index].Operation.Update.LayoutID).Scan(&count); err != nil {
					t.Fatal(err)
				}
				if name != wantName || count != wantHistory {
					t.Fatalf("save %d name=%s history=%d want=%s/%d", result.index, name, count, wantName, wantHistory)
				}
			}
			if repos[0].attempts.Load()+repos[1].attempts.Load() != 3 {
				t.Fatalf("expected one real deadlock retry: %d/%d", repos[0].attempts.Load(), repos[1].attempts.Load())
			}
		})
	}
}
