package controlplane

import (
	"context"
	"regexp"
	"strings"
	"testing"

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

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

	expectOrphanStandardActionSatelliteRows(mock)

	if err := VerifyActionTableConsistency(context.Background(), db); err != nil {
		t.Fatalf("VerifyActionTableConsistency() error = %v, want nil", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	expectOrphanStandardActionSatelliteRows(mock).
		AddRow("_System_ActionSchema", "sa_missing/input/leadId")

	err = VerifyActionTableConsistency(context.Background(), db)
	if err == nil {
		t.Fatal("VerifyActionTableConsistency() error = nil, want orphan satellite failure")
	}
	if !strings.Contains(err.Error(), "active standard-action satellite rows without a present (non-deleted) _System_StandardAction parent") ||
		!strings.Contains(err.Error(), "_System_ActionSchema sa_missing/input/leadId") {
		t.Fatalf("error = %q, want orphan satellite detail", err.Error())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

// TestVerifyActionTableConsistencyScopesCustomizedSatellites locks the contract
// that admin-customized (IsCustomized=1) rows of the three customizable
// satellites are excluded from the orphan check, while the L1-only internal-
// access-scope table is checked in full. A regression that drops the
// IsCustomized=0 predicate would fail-fail verify on a supported customization
// state (the original gate's BLOCK finding).
func TestVerifyActionTableConsistencyScopesCustomizedSatellites(t *testing.T) {
	for _, customizable := range []string{
		"_System_ActionSchema",
		"_System_ActionFieldDefault",
		"_System_ActionInputGuard",
	} {
		if !regexp.MustCompile(
			regexp.QuoteMeta(customizable) + `[\s\S]*?COALESCE\(s\.IsCustomized, 0\) = 0`,
		).MatchString(orphanStandardActionSatelliteQuery) {
			t.Fatalf("orphan query must scope %s to IsCustomized=0 so admin-customized orphans are preserved", customizable)
		}
	}

	scopeIdx := strings.Index(orphanStandardActionSatelliteQuery, "_System_StandardActionInternalAccessScope")
	if scopeIdx < 0 {
		t.Fatal("orphan query must still cover _System_StandardActionInternalAccessScope")
	}
	if strings.Contains(orphanStandardActionSatelliteQuery[scopeIdx:], "IsCustomized") {
		t.Fatal("L1-only _System_StandardActionInternalAccessScope must NOT be scoped by IsCustomized")
	}
}

// TestVerifyActionTableConsistencyKeysOnDeletionNotVisibility locks that the
// orphan gate treats parent presence (IsDeleted), not visibility (IsActive), as
// liveness. An admin-disabled (IsActive=0, IsDeleted=0) standard action keeps
// active satellites legitimately; flagging them would fail verify on a
// supported admin state. The dual of the reconcile cascade.
func TestVerifyActionTableConsistencyKeysOnDeletionNotVisibility(t *testing.T) {
	if strings.Contains(orphanStandardActionSatelliteQuery, "a.IsActive") {
		t.Fatal("orphan query must not treat parent IsActive as liveness — IsActive=0 is admin disable, not delete")
	}
	if !strings.Contains(orphanStandardActionSatelliteQuery, "a.IsDeleted") {
		t.Fatal("orphan query must gate parent presence on IsDeleted")
	}
}

func TestVerifyActionTableConsistencyRequiresDatabase(t *testing.T) {
	err := VerifyActionTableConsistency(context.Background(), nil)
	if err == nil {
		t.Fatal("VerifyActionTableConsistency(nil) error = nil, want database handle failure")
	}
	if !strings.Contains(err.Error(), "requires a database handle") {
		t.Fatalf("error = %q, want database handle detail", err.Error())
	}
}

func TestVerifyImplWiresActionTableConsistency(t *testing.T) {
	verifyImpl := parseControlplaneFuncDecl(t, "verify.go", "verifyImpl")
	if call := findFuncCallByName(verifyImpl, "VerifyActionTableConsistency"); call == nil {
		t.Fatal("verifyImpl must call VerifyActionTableConsistency so cmd/setup verify fails closed on action-table drift")
	}
}

func expectOrphanStandardActionSatelliteRows(mock sqlmock.Sqlmock) *sqlmock.Rows {
	rows := sqlmock.NewRows([]string{"Surface", "Sample"})
	mock.ExpectQuery(regexp.QuoteMeta(orphanStandardActionSatelliteQuery)).WillReturnRows(rows)
	return rows
}
