package recordlocks

import (
	"errors"
	"testing"
)

func TestOrderedParentKeysNormalizesDedupesAndSorts(t *testing.T) {
	keys := []ParentKey{
		{ObjectName: " Task__c ", RecordID: "task-2"},
		{ObjectName: "Account", RecordID: "acct-2"},
		{ObjectName: "account", RecordID: "acct-1"},
		{ObjectName: "Account", RecordID: "acct-2"},
		{ObjectName: "", RecordID: "ignored"},
		{ObjectName: "Case", RecordID: ""},
	}

	ordered := OrderedParentKeys(keys)
	want := []ParentKey{
		{ObjectName: "account", RecordID: "acct-1"},
		{ObjectName: "Account", RecordID: "acct-2"},
		{ObjectName: "Task__c", RecordID: "task-2"},
	}
	if len(ordered) != len(want) {
		t.Fatalf("OrderedParentKeys len = %d, want %d: %#v", len(ordered), len(want), ordered)
	}
	for i := range want {
		if ordered[i] != want[i] {
			t.Fatalf("OrderedParentKeys[%d] = %#v, want %#v; full=%#v", i, ordered[i], want[i], ordered)
		}
	}
}

func TestKeyIDDedupesObjectNameCase(t *testing.T) {
	left := KeyID(ParentKey{ObjectName: "Account", RecordID: "001"})
	right := KeyID(ParentKey{ObjectName: " account ", RecordID: "001"})
	if left != right {
		t.Fatalf("KeyID case/trim mismatch: %q != %q", left, right)
	}
}

func TestNormalizeParentKeyRejectsBlankParts(t *testing.T) {
	if _, ok := NormalizeParentKey(ParentKey{ObjectName: "Account"}); ok {
		t.Fatal("NormalizeParentKey accepted blank record ID")
	}
	if _, ok := NormalizeParentKey(ParentKey{RecordID: "001"}); ok {
		t.Fatal("NormalizeParentKey accepted blank object name")
	}
}

func TestScopeAllowsOnlyTransactionGlobalIncreasingOrder(t *testing.T) {
	scope := ScopeFromContext(WithScope(t.Context()))
	first, err := scope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "B"}})
	if err != nil {
		t.Fatalf("KeysToAcquire first: %v", err)
	}
	if len(first) != 1 {
		t.Fatalf("first acquire = %#v, want one key", first)
	}
	scope.MarkAcquired(first[0])

	duplicate, err := scope.KeysToAcquire([]ParentKey{{ObjectName: "account", RecordID: "B"}})
	if err != nil {
		t.Fatalf("KeysToAcquire duplicate: %v", err)
	}
	if len(duplicate) != 0 {
		t.Fatalf("duplicate acquire = %#v, want no-op", duplicate)
	}

	next, err := scope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "C"}})
	if err != nil {
		t.Fatalf("KeysToAcquire later key: %v", err)
	}
	if len(next) != 1 {
		t.Fatalf("later acquire = %#v, want one key", next)
	}

	if _, err := scope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "A"}}); !errors.Is(err, ErrOutOfOrderParentLock) {
		t.Fatalf("KeysToAcquire earlier key error = %v, want ErrOutOfOrderParentLock", err)
	}
	if !scope.HasAcquired(ParentKey{ObjectName: " account ", RecordID: "B"}) {
		t.Fatal("HasAcquired returned false for held key")
	}
	if scope.HasAcquired(ParentKey{ObjectName: "Account", RecordID: "A"}) {
		t.Fatal("HasAcquired returned true for unheld key")
	}
}

func TestScopeTransactionOwnedRowsBypassOnlyTheirOwnLaterLock(t *testing.T) {
	scope := ScopeFromContext(WithScope(t.Context()))
	scope.MarkAcquired(ParentKey{ObjectName: "User", RecordID: "user-001"})

	created := ParentKey{ObjectName: "ProcessInstance", RecordID: "pi-001"}
	scope.MarkTransactionOwned(created)

	duplicate, err := scope.KeysToAcquire([]ParentKey{created})
	if err != nil {
		t.Fatalf("KeysToAcquire transaction-owned duplicate: %v", err)
	}
	if len(duplicate) != 0 {
		t.Fatalf("transaction-owned duplicate acquire = %#v, want no-op", duplicate)
	}
	if !scope.HasAcquired(created) {
		t.Fatal("HasAcquired returned false for transaction-owned row")
	}
	acquired := scope.AcquiredKeys()
	if len(acquired) != 1 || acquired[0].ObjectName != "User" || acquired[0].RecordID != "user-001" {
		t.Fatalf("AcquiredKeys() = %#v, want only durable User parent", acquired)
	}

	if _, err := scope.KeysToAcquire([]ParentKey{{ObjectName: "ProcessInstance", RecordID: "pi-002"}}); !errors.Is(err, ErrOutOfOrderParentLock) {
		t.Fatalf("KeysToAcquire distinct earlier row error = %v, want ErrOutOfOrderParentLock", err)
	}
}

func TestWithScopeReplacesInheritedScope(t *testing.T) {
	staleCtx := WithScope(t.Context())
	staleScope := ScopeFromContext(staleCtx)
	first, err := staleScope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "B"}})
	if err != nil {
		t.Fatalf("KeysToAcquire stale first: %v", err)
	}
	staleScope.MarkAcquired(first[0])

	freshCtx := WithScope(staleCtx)
	freshScope := ScopeFromContext(freshCtx)
	if freshScope == nil {
		t.Fatal("WithScope returned context without scope")
	}
	if freshScope == staleScope {
		t.Fatal("WithScope reused inherited scope; want fresh transaction-attempt scope")
	}
	if keys, err := freshScope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "A"}}); err != nil || len(keys) != 1 {
		t.Fatalf("fresh scope KeysToAcquire earlier key = %#v, %v; want one allowed key", keys, err)
	}
}

func TestWithoutScopeMasksInheritedScope(t *testing.T) {
	inherited := WithScope(t.Context())
	if ScopeFromContext(inherited) == nil {
		t.Fatal("WithScope returned context without scope")
	}

	detached := WithoutScope(inherited)
	if scope := ScopeFromContext(detached); scope != nil {
		t.Fatalf("WithoutScope retained inherited scope %p, want nil", scope)
	}
}

func TestWithScopeForTransactionReusesSameIdentityAndReplacesDifferentIdentity(t *testing.T) {
	firstTx := &struct{ id string }{id: "first"}
	secondTx := &struct{ id string }{id: "second"}

	firstCtx := WithScopeForTransaction(t.Context(), firstTx)
	firstScope := ScopeFromContext(firstCtx)
	keys, err := firstScope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "B"}})
	if err != nil {
		t.Fatalf("KeysToAcquire first tx: %v", err)
	}
	firstScope.MarkAcquired(keys[0])

	sameTxCtx := WithScopeForTransaction(firstCtx, firstTx)
	sameTxScope := ScopeFromContext(sameTxCtx)
	if sameTxScope != firstScope {
		t.Fatal("WithScopeForTransaction replaced scope for the same transaction identity")
	}
	if _, err := sameTxScope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "A"}}); !errors.Is(err, ErrOutOfOrderParentLock) {
		t.Fatalf("same tx earlier key error = %v, want ErrOutOfOrderParentLock", err)
	}

	secondTxCtx := WithScopeForTransaction(firstCtx, secondTx)
	secondTxScope := ScopeFromContext(secondTxCtx)
	if secondTxScope == nil {
		t.Fatal("WithScopeForTransaction returned context without scope")
	}
	if secondTxScope == firstScope {
		t.Fatal("WithScopeForTransaction reused scope for a different transaction identity")
	}
	if keys, err := secondTxScope.KeysToAcquire([]ParentKey{{ObjectName: "Account", RecordID: "A"}}); err != nil || len(keys) != 1 {
		t.Fatalf("second tx KeysToAcquire earlier key = %#v, %v; want one allowed key", keys, err)
	}
}
