package metadata

import (
	"errors"
	"fmt"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
)

// TestWrapStrictCreateError_PassesConflictThroughUnwrapped is the create-side
// half of the #3484 client-visibility guarantee (test-plan (9)): a typed
// ConflictError from the strict-create chokepoint (an orphan-blocked create)
// must pass through unwrapped, NOT be blanket-wrapped in an InternalError. The
// contract sanitizes InternalError to an opaque 500 (IsInternal-first), which
// would hide both the 409 status and the reconcile remediation.
//
// Load-bearing: revert wrapStrictCreateError to a bare NewInternalError and this
// fails — the ConflictError no longer reaches errors.As.
func TestWrapStrictCreateError_PassesConflictThroughUnwrapped(t *testing.T) {
	orphan := apperrors.NewConflictErrorWithMessage("Widget__c",
		"object name \"Widget__c\" is blocked by an existing physical table with no live metadata; retry DELETE for that object name")

	got := wrapStrictCreateError(orphan, "failed to create schema")

	var conflict *apperrors.ConflictError
	if !errors.As(got, &conflict) {
		t.Fatalf("wrapStrictCreateError returned %T %v, want the *ConflictError to pass through", got, got)
	}
	if conflict.Error() != orphan.Error() {
		t.Fatalf("remediation message changed: got %q, want %q", conflict.Error(), orphan.Error())
	}
	// The wire message must carry the remediation verbatim (the Detail free-form
	// message, not the fixed "already exists" template).
	if !containsSub(conflict.Error(), "retry DELETE") {
		t.Fatalf("ConflictError wire text = %q, want it to carry the DELETE remediation", conflict.Error())
	}
}

// TestWrapStrictCreateError_WrapsOtherErrorsAsInternal verifies the negative:
// any non-conflict error keeps the InternalError wrap (a genuine infra failure
// must not masquerade as a 409).
func TestWrapStrictCreateError_WrapsOtherErrorsAsInternal(t *testing.T) {
	got := wrapStrictCreateError(errors.New("connection refused"), "failed to create schema")
	var conflict *apperrors.ConflictError
	if errors.As(got, &conflict) {
		t.Fatalf("a non-conflict error was surfaced as ConflictError: %v", got)
	}
	var internal *apperrors.InternalError
	if !errors.As(got, &internal) {
		t.Fatalf("wrapStrictCreateError returned %T, want *InternalError for a generic failure", got)
	}
}

// TestWrapDropTableError_PassesConflictThroughUnwrapped is the delete-side half:
// an inbound-FK-blocked DropTable (ConflictError naming the constraint) must
// reach the client unwrapped.
func TestWrapDropTableError_PassesConflictThroughUnwrapped(t *testing.T) {
	fkBlock := apperrors.NewConflictErrorWithMessage("Widget__c",
		"cannot delete object \"Widget__c\": referenced by fk_Gadget__c_Widget_Ref__c on Gadget__c")

	got := wrapDropTableError(fkBlock, "failed to drop table and schema")

	var conflict *apperrors.ConflictError
	if !errors.As(got, &conflict) {
		t.Fatalf("wrapDropTableError returned %T %v, want the *ConflictError to pass through", got, got)
	}
	if !containsSub(conflict.Error(), "fk_Gadget__c_Widget_Ref__c") {
		t.Fatalf("ConflictError wire text = %q, want the blocking constraint name", conflict.Error())
	}
}

// TestWrapDropTableError_WrapsTransientAsInternal verifies a transient
// (non-conflict) drop failure — e.g. a lock wait timeout in phase 2 — keeps the
// InternalError wrap.
func TestWrapDropTableError_WrapsTransientAsInternal(t *testing.T) {
	got := wrapDropTableError(fmt.Errorf("lock wait timeout exceeded"), "failed to drop table and schema")
	var internal *apperrors.InternalError
	if !errors.As(got, &internal) {
		t.Fatalf("wrapDropTableError returned %T, want *InternalError for a transient failure", got)
	}
}

func containsSub(s, sub string) bool {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return true
		}
	}
	return false
}
