package persistence

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

	"github.com/nexuscrm/shared/pkg/constants"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
)

func TestApplicationNavigationDeletionLifecycleOnTiDB(t *testing.T) {
	db, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()
	createCanonicalMetadataTables(t, db, constants.TableApp, navigationReceiptTable, constants.TableSetupAuditTrail,
		constants.TablePermissionSet, constants.TablePermissionSetAppVisibility, constants.TableProfileAppVisibility, PermissionStateVersionTable)
	if _, err := newPermissionStateRepositoryForTest(t, db, permissionStateLineage).EnsureInitialized(t.Context()); err != nil {
		t.Fatal(err)
	}
	metadata, coordinator := newPermissionStateMetadataDirectWriterRepository(t, db)
	tm := coordinator.transactions
	navigation := NewApplicationNavigationRepository(db, tm)
	ctx, cancel := context.WithTimeout(t.Context(), 40*time.Second)
	defer cancel()
	add := func(t *testing.T, id string) {
		t.Helper()
		_, err := db.ExecContext(ctx, "INSERT INTO `_System_App` (Id,Name,Label,FrontendAppId,Icon,Color,NavigationItems,NavigationRevision,UtilityItems,AssignedProfiles) VALUES (?,?,?,'owner','','',JSON_ARRAY(),1,JSON_ARRAY(),JSON_ARRAY())", id, id, id)
		if err != nil {
			t.Fatal(err)
		}
	}
	request := func(id string) semantic.SaveRequest {
		return semantic.SaveRequest{ApplicationID: id, ExpectedRevision: 1, IdempotencyKey: "same-request",
			Items: []semantic.Item{{ID: "start", Label: "Start", Target: semantic.Target{Page: &semantic.PageTarget{ID: "start"}}}}}
	}
	save := func(txCtx context.Context, id string) error {
		before, err := navigation.Lock(txCtx, "owner", id)
		if err != nil {
			return err
		}
		if _, found, err := navigation.Replay(txCtx, "owner", "actor", request(id)); err != nil || found {
			return err
		}
		_, err = navigation.Store(txCtx, "owner", "actor", request(id), before)
		return err
	}
	assertRows := func(t *testing.T, table, field, id string, want int) {
		t.Helper()
		var count int
		if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM `"+table+"` WHERE `"+field+"` = ?", id).Scan(&count); err != nil {
			t.Fatal(err)
		}
		if count != want {
			t.Fatalf("%s rows for %s = %d, want %d", table, id, count, want)
		}
	}
	for _, id := range []string{"app", "other"} {
		add(t, id)
		if err := tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error { return save(txCtx, id) }); err != nil {
			t.Fatal(err)
		}
	}
	rollback := errors.New("abort deletion")
	if err := tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		if err := metadata.DeleteApp(txCtx, "app"); err != nil {
			return err
		}
		return rollback
	}); !errors.Is(err, rollback) {
		t.Fatalf("delete rollback: %v", err)
	}
	assertRows(t, constants.TableApp, "Id", "app", 1)
	assertRows(t, navigationReceiptTable, "ApplicationId", "app", 1)
	if err := metadata.DeleteApp(ctx, "app"); err != nil {
		t.Fatal(err)
	}
	assertRows(t, constants.TableApp, "Id", "app", 0)
	assertRows(t, navigationReceiptTable, "ApplicationId", "app", 0)
	assertRows(t, navigationReceiptTable, "ApplicationId", "other", 1)
	assertRows(t, constants.TableSetupAuditTrail, "CreatedById", "actor", 2)
	add(t, "app")
	if err := tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error { return save(txCtx, "app") }); err != nil {
		t.Fatal(err)
	}
	current, err := navigation.Read(ctx, "owner", "app")
	if err != nil || current.Revision != 2 || len(current.Items) != 1 {
		t.Fatalf("recreated application did not save: %+v, %v", current, err)
	}
	assertRows(t, constants.TableSetupAuditTrail, "CreatedById", "actor", 3)

	// The same physical relation owns cleanup through generic record deletion.
	if err := NewRecordRepository(db).PhysicalDelete(ctx, constants.TableApp, "other"); err != nil {
		t.Fatal(err)
	}
	assertRows(t, navigationReceiptTable, "ApplicationId", "other", 0)
	assertRows(t, constants.TableSetupAuditTrail, "CreatedById", "actor", 3)

	for _, deleteFirst := range []bool{false, true} {
		name := "save-first"
		if deleteFirst {
			name = "delete-first"
		}
		t.Run(name, func(t *testing.T) {
			add(t, name)
			ready, release := make(chan struct{}), make(chan struct{})
			firstDone, secondDone := make(chan error, 1), make(chan error, 1)
			releaseFirst := sync.OnceFunc(func() { close(release) })
			defer releaseFirst()
			go func() {
				firstDone <- tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
					var err error
					if deleteFirst {
						err = metadata.DeleteApp(txCtx, name)
					} else {
						err = save(txCtx, name)
					}
					if err != nil {
						return err
					}
					close(ready)
					select {
					case <-release:
						return nil
					case <-ctx.Done():
						return ctx.Err()
					}
				})
			}()
			select {
			case <-ready:
			case err := <-firstDone:
				t.Fatalf("first operation: %v", err)
			case <-ctx.Done():
				t.Fatal(ctx.Err())
			}
			go func() {
				if deleteFirst {
					secondDone <- tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error { return save(txCtx, name) })
				} else {
					secondDone <- metadata.DeleteApp(ctx, name)
				}
			}()
			// Observe an actual database lock wait; a goroutine starting is not evidence
			// that the competing operation reached the application row.
			deadline := time.Now().Add(5 * time.Second)
			for {
				select {
				case err := <-secondDone:
					t.Fatalf("competing operation escaped the app lock: %v", err)
				default:
				}
				var waiting int
				if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM information_schema.DATA_LOCK_WAITS").Scan(&waiting); err != nil {
					t.Fatal(err)
				}
				if waiting > 0 {
					break
				}
				if time.Now().After(deadline) {
					t.Fatal("competing operation did not reach TiDB lock wait")
				}
				time.Sleep(20 * time.Millisecond)
			}
			releaseFirst()
			if err := <-firstDone; err != nil {
				t.Fatal(err)
			}
			err := <-secondDone
			if deleteFirst {
				if !errors.Is(err, ErrNavigationMissing) {
					t.Fatalf("save after deletion: %v", err)
				}
			} else if err != nil {
				t.Fatal(err)
			}
			assertRows(t, constants.TableApp, "Id", name, 0)
			assertRows(t, navigationReceiptTable, "ApplicationId", name, 0)
		})
	}
}
