package tenantoverlay

import (
	"context"
	"encoding/json"
	"regexp"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

// TestApplyObjectChildCloneRulesOverlay_FailsClosedOnZeroRows is the
// round-3 reviewer finding #2 guard. If _System_Object has no row for
// an apiName the seed declares ChildCloneRules on, the writer must
// hard-error instead of logging a warning and moving on. The pre-fix
// behavior would silently land a zero-length write and ValidateObject-
// ChildCloneRules (which only inspects rows that exist) would not
// catch the miss.
func TestApplyObjectChildCloneRulesOverlay_FailsClosedOnZeroRows(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	// Map iteration is nondeterministic; which apiName is first varies
	// across runs. Wire every declared object to return RowsAffected=0
	// so the first iteration always triggers the error path.
	pack := testOverlay(t)
	seedObjects, err := pack.objectAPINamesWithChildCloneRules()
	if err != nil {
		t.Fatalf("objectAPINamesWithChildCloneRules: %v", err)
	}
	mock.MatchExpectationsInOrder(false)
	for range seedObjects {
		mock.ExpectExec(regexp.QuoteMeta("UPDATE `_System_Object` SET `ChildCloneRules`")).
			WillReturnResult(sqlmock.NewResult(0, 0))
	}

	err = pack.applyObjectChildCloneRulesOverlay(context.Background(), db)
	if err == nil {
		t.Fatalf("expected hard-fail on 0 rows updated, got nil")
	}
	if !strings.Contains(err.Error(), "updated 0 rows") ||
		!strings.Contains(err.Error(), "apply-tenant ordering") {
		t.Fatalf("error should call out the ordering invariant; got %v", err)
	}
}

// TestApplyObjectChildCloneRulesOverlay_ExplicitEmptyArrayClear proves
// that an empty rule list for an object writes "[]" rather than being
// skipped. "Skip on empty" was the old behavior; it made a deliberate
// seed-side removal invisible to apply-tenant, leaving stale rules
// alive on upgraded orgs.
func TestApplyObjectChildCloneRulesOverlay_ExplicitEmptyArrayClear(t *testing.T) {
	pack := tempOverlayWithFile(t, ObjectChildCloneRulesFile, []byte(`{"Quote": []}`))

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	// Must still issue the UPDATE, with the literal "[]" payload.
	mock.ExpectExec(regexp.QuoteMeta("UPDATE `_System_Object` SET `ChildCloneRules`")).
		WithArgs("[]", sqlmock.AnyArg()).
		WillReturnResult(sqlmock.NewResult(0, 1))

	if err := pack.applyObjectChildCloneRulesOverlay(context.Background(), db); err != nil {
		t.Fatalf("applyObjectChildCloneRulesOverlay on empty array: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

// TestApplyObjectChildCloneRulesOverlay_NilListClears ensures a null
// value in the JSON (e.g. someone typed `"Quote": null`) lands as an
// empty array. Defensive against seed edits that round-trip nil lists.
func TestApplyObjectChildCloneRulesOverlay_NilListClears(t *testing.T) {
	pack := tempOverlayWithFile(t, ObjectChildCloneRulesFile, []byte(`{"Quote": null}`))

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectExec(regexp.QuoteMeta("UPDATE `_System_Object` SET `ChildCloneRules`")).
		WithArgs("[]", sqlmock.AnyArg()).
		WillReturnResult(sqlmock.NewResult(0, 1))

	if err := pack.applyObjectChildCloneRulesOverlay(context.Background(), db); err != nil {
		t.Fatalf("applyObjectChildCloneRulesOverlay on nil list: %v", err)
	}
	// Sanity: the written payload round-trips to an empty slice.
	var back []struct{}
	if err := json.Unmarshal([]byte(`[]`), &back); err != nil || back == nil {
		t.Fatalf("sanity: empty array must parse to non-nil slice; got err=%v slice=%v", err, back)
	}
}
