package persistence

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	database "github.com/nexuscrm/db"
	pkgerrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	"github.com/nexuscrm/shared/pkg/models"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
	"sync"
	"testing"
	"time"
)

func TestApplicationNavigationRejectsLegacyOverwriteOnTiDB(t *testing.T) {
	raw, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()
	createCanonicalMetadataTables(t, raw, "_System_App", navigationReceiptTable, "_System_SetupAuditTrail")
	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
	defer cancel()
	_, err := raw.ExecContext(ctx, "INSERT INTO `_System_App` (Id,Name,Label,FrontendAppId,Icon,Color,NavigationItems,UtilityItems,AssignedProfiles) VALUES ('app','app','App','owner','','',JSON_ARRAY(),JSON_ARRAY(),JSON_ARRAY())")
	if err != nil {
		t.Fatal(err)
	}
	manager := NewTransactionManager(database.NewConnectionFromDB(raw))
	navigation := NewApplicationNavigationRepository(raw, manager)
	items := []semantic.Item{{ID: "page", Label: "Rich page", Target: semantic.Target{Page: &semantic.PageTarget{ID: "registered"}}}}
	if err := manager.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		_, err := navigation.Initialize(txCtx, "owner", "app", items, NavigationSourceDigest([]byte("[]")))
		return err
	}); err != nil {
		t.Fatal(err)
	}
	repository, coordinator := newPermissionStateMetadataDirectWriterRepository(t, raw)
	write := func(app *models.AppConfig) error {
		return coordinator.transactions.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
			return repository.updateAppWithExec(txCtx, repository.GetExecutor(txCtx), "app", app)
		})
	}
	for _, legacy := range [][]models.NavigationItem{{}, {{ID: "old", Type: "page", Label: "Old", PageURL: "/old"}}} {
		err := write(&models.AppConfig{ID: "app", FrontendAppID: "owner", Label: "Must not commit", NavigationItems: legacy})
		var validation *pkgerrors.ValidationError
		if !errors.As(err, &validation) || validation.Field != "NavigationItems" {
			t.Fatalf("expected legacy navigation rejection, got %v", err)
		}
	}
	for _, owner := range []string{"other", "OWNER", ""} {
		err := write(&models.AppConfig{ID: "app", FrontendAppID: owner, Label: "Must not rebind"})
		var validation *pkgerrors.ValidationError
		if !errors.As(err, &validation) || validation.Field != "FrontendAppId" {
			t.Fatalf("expected owner rebind rejection for %q, got %v", owner, err)
		}
	}
	if err := write(&models.AppConfig{ID: "app", FrontendAppID: "owner", Label: "Updated shell"}); err != nil {
		t.Fatal(err)
	}
	current, err := navigation.Read(ctx, "owner", "app")
	if err != nil {
		t.Fatal(err)
	}
	if current.Revision != 1 || len(current.Items) != 1 || current.Items[0].Target.Page == nil || *current.Items[0].Target.Page != *items[0].Target.Page {
		t.Fatalf("shell edit changed navigation: %+v", current)
	}
	var label string
	if err := raw.QueryRowContext(ctx, "SELECT Label FROM `_System_App` WHERE Id='app'").Scan(&label); err != nil || label != "Updated shell" {
		t.Fatalf("shell label=%q err=%v", label, err)
	}
}

func TestApplicationNavigationTransactionsOnTiDB(t *testing.T) {
	raw, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()
	createCanonicalMetadataTables(t, raw, "_System_App", navigationReceiptTable, "_System_SetupAuditTrail")
	const app = "navigation-test-app"
	const owner = "test.application"
	if _, err := raw.Exec("INSERT INTO `_System_App` (Id, Name, Label, FrontendAppId, Icon, Color, NavigationItems, UtilityItems, AssignedProfiles) VALUES (?, ?, ?, ?, 'App', '#000000', JSON_ARRAY(), JSON_ARRAY(), JSON_ARRAY())", app, app, "Application", owner); err != nil {
		t.Fatal(err)
	}
	tm := NewTransactionManager(database.NewConnectionFromDB(raw))
	repo := NewApplicationNavigationRepository(raw, tm)
	ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
	defer cancel()
	original := []semantic.Item{{ID: "start", Label: "Start", Target: semantic.Target{Page: &semantic.PageTarget{ID: "start"}}}}
	if err := tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		changed, err := repo.Initialize(txCtx, owner, app, original, NavigationSourceDigest([]byte("[]")))
		if !changed && err == nil {
			t.Error("initialization did not change state")
		}
		return err
	}); err != nil {
		t.Fatal(err)
	}
	if err := VerifyApplicationNavigationSchema(ctx, raw); err != nil {
		t.Fatal(err)
	}
	request := semantic.SaveRequest{ApplicationID: app, ExpectedRevision: 1, IdempotencyKey: "remove-start", Items: []semantic.Item{}}
	save := func(repository *ApplicationNavigationRepository, manager *TransactionManager, r semantic.SaveRequest) (NavigationMutationResult, error) {
		var result NavigationMutationResult
		err := manager.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
			before, err := repository.Lock(txCtx, owner, app)
			if err != nil {
				return err
			}
			replay, found, err := repository.Replay(txCtx, owner, "actor", r)
			if err != nil {
				return err
			}
			if found {
				result = replay
				return nil
			}
			result, err = repository.Store(txCtx, owner, "actor", r, before)
			return err
		})
		return result, err
	}
	secondTM := NewTransactionManager(database.NewConnectionFromDB(raw))
	second := NewApplicationNavigationRepository(raw, secondTM)
	alternate := request
	alternate.IdempotencyKey = "rename-start"
	alternate.Items = append([]semantic.Item{}, original...)
	alternate.Items[0].Label = "Renamed"
	type outcome struct {
		r       NavigationMutationResult
		err     error
		request semantic.SaveRequest
	}
	outcomes := make(chan outcome, 2)
	start := make(chan struct{})
	var wg sync.WaitGroup
	for _, attempt := range []struct {
		repo *ApplicationNavigationRepository
		tm   *TransactionManager
		r    semantic.SaveRequest
	}{{repo, tm, request}, {second, secondTM, alternate}} {
		wg.Add(1)
		go func(a struct {
			repo *ApplicationNavigationRepository
			tm   *TransactionManager
			r    semantic.SaveRequest
		}) {
			defer wg.Done()
			<-start
			result, err := save(a.repo, a.tm, a.r)
			outcomes <- outcome{result, err, a.r}
		}(attempt)
	}
	close(start)
	wg.Wait()
	close(outcomes)
	var winner outcome
	success, conflict := 0, 0
	for o := range outcomes {
		if o.err == nil {
			success++
			winner = o
		} else if errors.Is(o.err, ErrNavigationConflict) {
			conflict++
		} else {
			t.Fatalf("save failed unexpectedly: %v", o.err)
		}
	}
	if success != 1 || conflict != 1 {
		t.Fatalf("success=%d conflict=%d", success, conflict)
	}
	read, err := second.Read(ctx, owner, app)
	if err != nil || read.Revision != 2 {
		t.Fatalf("cross-instance read=%+v err=%v", read, err)
	}
	replay, err := save(repo, tm, winner.request)
	if err != nil || replay.Document.Revision != 2 {
		t.Fatalf("committed response replay=%+v err=%v", replay, err)
	}
	wrong := winner.request
	wrong.Items = []semantic.Item{{ID: "different", Label: "Different", Target: semantic.Target{Resource: &semantic.ResourceTarget{ResourceType: "Thing"}}}}
	if _, err := save(repo, tm, wrong); !errors.Is(err, ErrNavigationRequestConflict) {
		t.Fatalf("same-key mutation err=%v", err)
	}
	// Empty is a deliberate current document, not an initialization opportunity.
	empty := request
	empty.ExpectedRevision = read.Revision
	empty.IdempotencyKey = "empty-current"
	emptied, err := save(repo, tm, empty)
	if err != nil {
		t.Fatal(err)
	}
	if err := tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		changed, err := repo.Initialize(txCtx, owner, app, original, NavigationSourceDigest([]byte("[]")))
		if changed {
			t.Error("initializer overwrote current empty navigation")
		}
		return err
	}); err != nil {
		t.Fatal(err)
	}
	final, err := second.Read(ctx, owner, app)
	if err != nil || final.Items == nil || len(final.Items) != 0 || final.Revision != emptied.Document.Revision {
		t.Fatalf("empty not preserved: %+v %v", final, err)
	}
	beforeFailure := final.Revision
	failure := errors.New("injected failure before commit")
	rollback := alternate
	rollback.ExpectedRevision = final.Revision
	rollback.IdempotencyKey = "rollback"
	err = tm.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		before, e := repo.Lock(txCtx, owner, app)
		if e != nil {
			return e
		}
		if _, e = repo.Store(txCtx, owner, "actor", rollback, before); e != nil {
			return e
		}
		return failure
	})
	if !errors.Is(err, failure) {
		t.Fatalf("rollback err=%v", err)
	}
	final, err = repo.Read(ctx, owner, app)
	if err != nil || final.Revision != beforeFailure || len(final.Items) != 0 {
		t.Fatalf("partial mutation survived rollback: %+v %v", final, err)
	}
	if _, err := repo.Read(ctx, "another.application", app); !errors.Is(err, ErrNavigationMissing) {
		t.Fatalf("cross-owner read err=%v", err)
	}
	var receipts int
	if err := raw.QueryRow("SELECT COUNT(*) FROM `_System_ApplicationNavigationReceipt` WHERE ApplicationId = ?", app).Scan(&receipts); err != nil {
		t.Fatal(err)
	}
	if receipts != 2 {
		t.Fatalf("receipts=%d, want winner and empty (no duplicate or rolled-back receipt)", receipts)
	}
	var auditRows int
	if err := raw.QueryRow("SELECT COUNT(*) FROM `_System_SetupAuditTrail`").Scan(&auditRows); err != nil {
		t.Fatal(err)
	}
	wantAudits := 1
	if emptied.Changed {
		wantAudits++
	}
	if auditRows != wantAudits {
		t.Fatalf("audit rows=%d want=%d: replay, conflict, no-op and rollback must not add changes", auditRows, wantAudits)
	}
	var display, actor string
	if err := raw.QueryRow("SELECT Display, CreatedById FROM `_System_SetupAuditTrail` ORDER BY CreatedDate, Id LIMIT 1").Scan(&display, &actor); err != nil {
		t.Fatal(err)
	}
	var change navigationAuditChange
	if err := json.Unmarshal([]byte(display), &change); err != nil {
		t.Fatal(err)
	}
	if change.OwnerID != owner || change.ApplicationID != app || actor != "actor" || change.Before.Revision+1 != change.After.Revision {
		t.Fatalf("audit lost mutation identity or revisions: %+v actor=%s", change, actor)
	}
	// Fail the real audit INSERT after the navigation UPDATE, then retry the
	// identical command. No document or receipt may survive the failed audit.
	if _, err := raw.Exec("DROP TABLE `_System_SetupAuditTrail`"); err != nil {
		t.Fatal(err)
	}
	rollback.IdempotencyKey = "audit-unavailable"
	if _, err := save(repo, tm, rollback); err == nil {
		t.Fatal("save succeeded without an audit trail")
	}
	unchanged, err := repo.Read(ctx, owner, app)
	if err != nil || unchanged.Revision != final.Revision || len(unchanged.Items) != 0 {
		t.Fatalf("audit failure committed navigation: %+v %v", unchanged, err)
	}
	createCanonicalMetadataTables(t, raw, "_System_SetupAuditTrail")
	if _, err := save(repo, tm, rollback); err != nil {
		t.Fatalf("retry after restoring audit: %v", err)
	}
	if err := raw.QueryRow("SELECT COUNT(*) FROM `_System_SetupAuditTrail`").Scan(&auditRows); err != nil || auditRows != 1 {
		t.Fatalf("retry audit rows=%d err=%v", auditRows, err)
	}

}

func TestApplicationNavigationReadUsesCallerTransaction(t *testing.T) {
	raw, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()
	createCanonicalMetadataTables(t, raw, "_System_App", navigationReceiptTable, "_System_SetupAuditTrail")
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	_, err := raw.Exec("INSERT INTO `_System_App` (Id,Name,Label,FrontendAppId,Icon,Color,NavigationItems,NavigationRevision,UtilityItems,AssignedProfiles) VALUES ('app','app','App','owner','','',JSON_ARRAY(),1,JSON_ARRAY(),JSON_ARRAY())")
	if err != nil {
		t.Fatal(err)
	}
	tm := NewTransactionManager(database.NewConnectionFromDB(raw))
	repo := NewApplicationNavigationRepository(raw, tm)
	rollback := errors.New("rollback read transaction")
	err = tm.WithTransaction(ctx, func(txCtx context.Context) error {
		tx := tm.ExtractTx(txCtx).(Executor)
		if _, e := tx.ExecContext(txCtx, "UPDATE `_System_App` SET NavigationRevision=2 WHERE Id='app'"); e != nil {
			return e
		}
		inside, e := repo.Read(txCtx, "owner", "app")
		if e != nil {
			return e
		}
		many, e := repo.ReadMany(txCtx, "owner", []string{"app"})
		if e != nil {
			return e
		}
		outside, e := repo.Read(ctx, "owner", "app")
		if e != nil {
			return e
		}
		if inside.Revision != 2 || len(many) != 1 || many[0].Revision != 2 || outside.Revision != 1 {
			t.Fatalf("transaction leaked: inside=%+v many=%+v outside=%+v", inside, many, outside)
		}
		if _, e := repo.Lock(txCtx, "owner", "app"); e == nil {
			t.Fatal("ordinary read transaction accepted as mutation owner")
		}
		return rollback
	})
	if !errors.Is(err, rollback) {
		t.Fatal(err)
	}
}

func TestApplicationDefinitionsDoNotReadCurrentNavigation(t *testing.T) {
	raw, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()
	createCanonicalMetadataTables(t, raw, "_System_App")
	_, err := raw.Exec("INSERT INTO `_System_App` (Id,Name,Label,FrontendAppId,Icon,Color,NavigationItems,UtilityItems,AssignedProfiles) VALUES ('app','app','App','owner','','',JSON_OBJECT('not','a legacy array'),JSON_ARRAY(),JSON_ARRAY())")
	if err != nil {
		t.Fatal(err)
	}
	repository := NewMetadataRepository(raw)
	definitions, err := repository.GetApplicationDefinitions(context.Background())
	if err != nil || len(definitions) != 1 || definitions[0].NavigationItems == nil || len(definitions[0].NavigationItems) != 0 {
		t.Fatalf("Catalog read mutable navigation: %+v %v", definitions, err)
	}
}

func TestApplicationNavigationSchemaRequiresReceiptPrimaryKeyOnTiDB(t *testing.T) {
	raw, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()
	createCanonicalMetadataTables(t, raw, "_System_App", navigationReceiptTable, "_System_SetupAuditTrail")
	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
	defer cancel()
	if err := VerifyApplicationNavigationSchema(ctx, raw); err != nil {
		t.Fatal(err)
	}
	// Preserve canonical columns but omit the receipt primary key.
	definitions, err := schemacatalog.SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	if _, err := raw.ExecContext(ctx, "DROP TABLE `_System_ApplicationNavigationReceipt`"); err != nil {
		t.Fatal(err)
	}
	for _, definition := range definitions {
		if definition.TableName != navigationReceiptTable {
			continue
		}
		for i := range definition.Columns {
			definition.Columns[i].PrimaryKey = false
		}
		if err := NewSchemaRepository(raw, nil).CreatePhysicalTable(ctx, definition); err != nil {
			t.Fatal(err)
		}
	}
	if err := VerifyApplicationNavigationSchema(ctx, raw); err == nil {
		t.Fatal("navigation readiness accepted a receipt table without its primary key")
	}
	if _, err := raw.ExecContext(ctx, "ALTER TABLE `_System_ApplicationNavigationReceipt` ADD PRIMARY KEY (Id)"); err != nil {
		t.Fatal(err)
	}
	if err := VerifyApplicationNavigationSchema(ctx, raw); err != nil {
		t.Fatal(err)
	}
	var constraint string
	if err := raw.QueryRowContext(ctx, "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE CONSTRAINT_SCHEMA=DATABASE() AND TABLE_NAME='_System_ApplicationNavigationReceipt' AND COLUMN_NAME='ApplicationId' AND REFERENCED_TABLE_NAME='_System_App'").Scan(&constraint); err != nil {
		t.Fatal(err)
	}
	if _, err := raw.ExecContext(ctx, fmt.Sprintf("ALTER TABLE `_System_ApplicationNavigationReceipt` DROP FOREIGN KEY `%s`", constraint)); err != nil {
		t.Fatal(err)
	}
	if err := VerifyApplicationNavigationSchema(ctx, raw); err == nil {
		t.Fatal("navigation readiness accepted receipts without lifecycle cleanup")
	}
}
