package persistence

import (
	"context"
	"errors"
	"testing"

	"github.com/go-sql-driver/mysql"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/recordlocks"
)

var sentinelErr = errors.New("sentinel")

func TestDetachTxMasksInheritedParentLockScope(t *testing.T) {
	txm := &TransactionManager{}
	ctx := recordlocks.WithScope(context.Background())
	wantPrelock := recordlocks.ParentKey{ObjectName: "Campaign", RecordID: "campaign-001"}
	ctx = recordlocks.WithForcedParentPrelocks(ctx, []recordlocks.ParentKey{wantPrelock})
	ctx = ports.WithInitialHistorySequenceLocks(ctx, []string{"fld-campaign-name"})
	ctx = ports.WithInitialHistoryBusinessRowObjects(ctx, []string{"Campaign"})
	detached := txm.DetachTx(ctx)
	if scope := recordlocks.ScopeFromContext(detached); scope != nil {
		t.Fatalf("DetachTx retained inherited parent lock scope %p", scope)
	}
	if forced := recordlocks.ForcedParentPrelocksFromContext(detached); len(forced) != 1 || forced[0] != wantPrelock {
		t.Fatalf("DetachTx forced prelocks = %#v, want %#v", forced, []recordlocks.ParentKey{wantPrelock})
	}
	if fields := ports.InitialHistorySequenceLocks(detached); len(fields) != 0 {
		t.Fatalf("DetachTx initial history fields = %v, want none", fields)
	}
	if ports.RequiresInitialHistoryBusinessRowPrelock(detached, "Campaign") {
		t.Fatal("DetachTx retained initial history business-row coverage")
	}
}

func TestDetachTxStripsAfterCommitHooks(t *testing.T) {
	tm := &TransactionManager{}
	outerCtx := ports.WithAfterCommitHooks(context.Background())
	outerCtx = ports.WithTransactionCompletionHooks(outerCtx)
	outerCalls := 0
	if !ports.RegisterAfterCommitHook(outerCtx, func() {
		outerCalls++
	}) {
		t.Fatal("RegisterAfterCommitHook on outer tx context = false, want true")
	}
	outerCompletionCalls := 0
	if !ports.RegisterTransactionCompletionHook(outerCtx, func(ports.TransactionCompletionOutcome) {
		outerCompletionCalls++
	}) {
		t.Fatal("RegisterTransactionCompletionHook on outer tx context = false, want true")
	}

	detached := tm.DetachTx(outerCtx)
	if ports.RegisterAfterCommitHook(detached, func() {
		t.Fatal("detached context must not accept hooks on the outer collector")
	}) {
		t.Fatal("RegisterAfterCommitHook on detached context = true, want false")
	}
	if ports.RegisterTransactionCompletionHook(detached, func(ports.TransactionCompletionOutcome) {
		t.Fatal("detached context must not accept completion hooks on the outer collector")
	}) {
		t.Fatal("RegisterTransactionCompletionHook on detached context = true, want false")
	}
	innerCtx := ports.WithAfterCommitHooks(detached)
	innerCtx = ports.WithTransactionCompletionHooks(innerCtx)
	innerCalls := 0
	if !ports.RegisterAfterCommitHook(innerCtx, func() {
		innerCalls++
	}) {
		t.Fatal("RegisterAfterCommitHook on inner tx context = false, want true")
	}
	innerCompletionCalls := 0
	if !ports.RegisterTransactionCompletionHook(innerCtx, func(ports.TransactionCompletionOutcome) {
		innerCompletionCalls++
	}) {
		t.Fatal("RegisterTransactionCompletionHook on inner tx context = false, want true")
	}

	ports.RunTransactionCompletionHooks(innerCtx, ports.TransactionCompletionCommitted)
	if innerCompletionCalls != 1 {
		t.Fatalf("inner completion hook calls = %d, want 1", innerCompletionCalls)
	}
	ports.RunAfterCommitHooks(innerCtx)
	if innerCalls != 1 {
		t.Fatalf("inner hook calls = %d, want 1", innerCalls)
	}
	if outerCalls != 0 || outerCompletionCalls != 0 {
		t.Fatalf("outer hooks ran during inner commit, afterCommit=%d completion=%d", outerCalls, outerCompletionCalls)
	}

	ports.RunTransactionCompletionHooks(outerCtx, ports.TransactionCompletionCommitted)
	if outerCompletionCalls != 1 {
		t.Fatalf("outer completion hook calls after outer completion = %d, want 1", outerCompletionCalls)
	}
	ports.RunAfterCommitHooks(outerCtx)
	if outerCalls != 1 {
		t.Fatalf("outer hook calls after outer commit = %d, want 1", outerCalls)
	}
}

// F6a: IsLockContention (formerly isDeadlock) covers both 1205 and 1213.
func TestIsLockContention(t *testing.T) {
	for _, tc := range []struct {
		name string
		err  error
		want bool
	}{
		{
			name: "typed 1205 lock-wait timeout",
			err:  &mysql.MySQLError{Number: 1205, Message: "Lock wait timeout exceeded; try restarting transaction"},
			want: true,
		},
		{
			name: "typed 1213 deadlock",
			err:  &mysql.MySQLError{Number: 1213, Message: "Deadlock found when trying to get lock; try restarting transaction"},
			want: true,
		},
		{
			name: "typed parent-lane contention",
			err:  apperrors.NewLockContentionError(ErrParentLaneContention),
			want: true,
		},
		{
			name: "transaction abort wrapping parent-lane contention",
			err:  ports.NewTransactionAbortedError(ErrParentLaneContention),
			want: true,
		},
		{
			name: "transaction abort without contention cause",
			err:  ports.NewTransactionAbortedError(errors.New("heartbeat persistence failed")),
			want: false,
		},
		{
			name: "string fallback Error 1205",
			err:  errors.New("Error 1205: Lock wait timeout exceeded"),
			want: true,
		},
		{
			name: "string fallback Error 1213",
			err:  errors.New("Error 1213: Deadlock found"),
			want: true,
		},
		{
			name: "lowercase fold DEADLOCK FOUND",
			err:  errors.New("DEADLOCK FOUND"),
			want: true,
		},
		{
			name: "lowercase fold LOCK WAIT TIMEOUT",
			err:  errors.New("LOCK WAIT TIMEOUT"),
			want: true,
		},
		{
			name: "random failure mentioning 1205 as data",
			err:  errors.New("record 1205 failed validation"),
			want: false,
		},
		{
			name: "random failure",
			err:  errors.New("random failure"),
			want: false,
		},
		{
			name: "nil error",
			err:  nil,
			want: false,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got := IsLockContention(tc.err)
			if got != tc.want {
				t.Errorf("IsLockContention(%v) = %v, want %v", tc.err, got, tc.want)
			}
		})
	}
}

func TestLockContentionClassifiersSeparateParentLaneAndDBRow(t *testing.T) {
	parentLaneErr := apperrors.NewLockContentionError(ErrParentLaneContention)
	dbRowErr := apperrors.NewLockContentionError(&mysql.MySQLError{Number: 1205, Message: "Lock wait timeout exceeded"})
	otherErr := errors.New("business record 1205 failed validation")

	if !IsParentLaneContention(parentLaneErr) {
		t.Fatal("IsParentLaneContention(parentLaneErr) = false, want true")
	}
	if IsDBRowLockContention(parentLaneErr) {
		t.Fatal("IsDBRowLockContention(parentLaneErr) = true, want false")
	}
	if !IsDBRowLockContention(dbRowErr) {
		t.Fatal("IsDBRowLockContention(dbRowErr) = false, want true")
	}
	if IsParentLaneContention(dbRowErr) {
		t.Fatal("IsParentLaneContention(dbRowErr) = true, want false")
	}
	if IsLockContention(otherErr) {
		t.Fatal("IsLockContention(otherErr) = true, want false")
	}
}
