package automationintegration_test

// TestFlowInterviewElevation_CustomProfileCanWriteWithoutPermission is a
// regression test for issue #1619 that validates the corrected scoped
// elevation boundary.
//
// Before the fix: FlowInterview bookkeeping writes ran under the caller's
// user context. A user on a CUSTOM profile with no FlowInterview
// ObjectPermissions row hit a CRUD permission error on CreateInterview
// (Insert) returning HTTP 500 to the caller.
//
// After the fix (scoped elevation): elevation is confined to the internal
// fresh-execution call sites only.
//
//   - CreateInterview stays elevated in-method: the sole caller is
//     executeFlowVersion (fresh exec), not the user-facing resumeFlow path.
//     Custom-profile users can create interviews (the fatal #1619 write).
//
//   - CompleteInterview is bare in-method: it is dual-use (fresh exec +
//     resumeFlow). The fresh-execution path elevates at the call site;
//     the user-facing resumeFlow path deliberately does NOT elevate.
//
// This test verifies both sides of the boundary:
//
//	(a) CreateInterview(userCtx)    SUCCEEDS for a custom-profile user.
//	(b) CompleteInterview(userCtx)  FAILS  for a custom-profile user (bare ctx).
//	(c) CompleteInterview(elevCtx)  SUCCEEDS (elevated ctx = fresh-exec path).
import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestFlowInterviewElevation_CustomProfileCanWriteWithoutPermission(t *testing.T) {
	sysCtx := constants.WithSecurityContext(
		context.Background(),
		constants.SystemContext,
		"issue-1619 FlowInterview elevation regression test",
	)

	// Use the base L0 harness — no app-pack install needed for this test.
	// FlowInterview is an L0 system table created by InitL0.
	conn, sm := setupRecordTriggerRuntime(t, sysCtx)
	defer func() { _ = conn.Close() }()

	db := conn.DB()

	// ---------------------------------------------------------------
	// 1. Forge a CUSTOM profile with no FlowInterview ObjectPermissions.
	//    We intentionally do NOT grant FlowInterview write — that is the
	//    exact scenario that caused HTTP 500 before the fix.
	// ---------------------------------------------------------------
	customProfileID := fmt.Sprintf("custprof-%d", time.Now().UnixNano())
	if _, err := db.ExecContext(sysCtx,
		`INSERT IGNORE INTO Profile (Id, Name, Description, IsActive, IsSystem, CreatedDate, LastModifiedDate)
		 VALUES (?, ?, 'Custom profile for issue-1619 elevation test', 1, 0, NOW(), NOW())`,
		customProfileID, "CustomElevationTest",
	); err != nil {
		t.Fatalf("INSERT custom Profile: %v", err)
	}
	t.Logf("Custom profile id: %s", customProfileID)

	// Sanity check: must not carry the super-user capability.
	if sm.Permissions.HasSuperUserCapability(sysCtx, &models.UserSession{ID: "probe", ProfileID: customProfileID}) {
		t.Fatalf("custom profile %q unexpectedly carries the super-user capability", customProfileID)
	}

	// Create the profile-owned PermissionSet (required for any CRUD check).
	customPermSetID, err := sm.Permissions.EnsureProfilePermissionSet(sysCtx, customProfileID, "CustomElevationTest")
	if err != nil {
		t.Fatalf("EnsureProfilePermissionSet(custom): %v", err)
	}
	if customPermSetID == "" {
		t.Fatal("EnsureProfilePermissionSet returned empty permSetID")
	}
	t.Logf("Custom profile permSetID: %s", customPermSetID)

	// Confirm NO FlowInterview ObjectPermissions row exists for this profile.
	var flowInterviewPermRows int
	if err := db.QueryRowContext(sysCtx,
		`SELECT COUNT(*) FROM ObjectPermissions WHERE ParentId = ? AND SobjectType = 'FlowInterview' AND IsDeleted = 0`,
		customPermSetID,
	).Scan(&flowInterviewPermRows); err != nil {
		t.Fatalf("query FlowInterview ObjectPermissions: %v", err)
	}
	if flowInterviewPermRows != 0 {
		t.Fatalf("expected 0 FlowInterview ObjectPermissions for custom profile, got %d — test precondition violated", flowInterviewPermRows)
	}
	t.Log("PRECONDITION: custom profile has 0 FlowInterview ObjectPermissions rows")

	// Refresh permission cache so the new profile is visible.
	if err := sm.Permissions.RefreshPermissions(sysCtx); err != nil {
		t.Fatalf("RefreshPermissions: %v", err)
	}

	// ---------------------------------------------------------------
	// 2. Create a User on the custom profile.
	// ---------------------------------------------------------------
	customEmail := fmt.Sprintf("custom.elev.%d@example.test", time.Now().UnixNano())
	customUserRecord, err := sm.Persistence.Insert(sysCtx, "User", models.SObject{
		"FirstName": "CustomElevation",
		"LastName":  "Tester",
		"Email":     customEmail,
		"Password":  "TempPassw0rd!",
		"ProfileId": customProfileID,
	}, systemAdminSession())
	if err != nil {
		t.Fatalf("insert custom User: %v", err)
	}
	customUserID, _ := customUserRecord["Id"].(string)
	if customUserID == "" {
		t.Fatal("insert custom User returned empty Id")
	}
	t.Logf("Custom user id: %s", customUserID)

	customUser := &models.UserSession{
		ID:            customUserID,
		Name:          "CustomElevation Tester",
		ProfileID:     customProfileID,
		IsSystemAdmin: false,
	}

	// ---------------------------------------------------------------
	// 3. Insert a minimal Flow row so that FlowInterview.FlowVersionId
	//    satisfies the referential-integrity check.
	// ---------------------------------------------------------------
	fakeFlowVersionID := uuid.New().String()
	if _, err := db.ExecContext(sysCtx,
		`INSERT IGNORE INTO Flow (Id, DefinitionId, MasterLabel, VersionNumber, Status, ProcessType, IsDeleted, CreatedDate, LastModifiedDate)
		 VALUES (?, ?, 'issue-1619 elevation test flow', 1, 'Active', 'AutoLaunchedFlow', 0, NOW(), NOW())`,
		fakeFlowVersionID, uuid.New().String(),
	); err != nil {
		t.Fatalf("INSERT fake Flow row: %v", err)
	}
	t.Logf("Inserted fake Flow row id: %s", fakeFlowVersionID)

	// ---------------------------------------------------------------
	// (a) CreateInterview(userCtx) must SUCCEED.
	//     CreateInterview stays elevated in-method — sole caller is the
	//     fresh-execution path. This is the fatal #1619 write.
	// ---------------------------------------------------------------
	userCtx := constants.WithSecurityContext(
		context.Background(),
		constants.UserContext,
		"issue-1619 custom-profile user context",
	)
	interview, err := sm.FlowInterviewSvc.CreateInterview(userCtx, fakeFlowVersionID, "issue-1619 elevation regression", customUser)
	if err != nil {
		t.Fatalf("(a) CreateInterview failed for custom-profile user: %v\n"+
			"  → This is the issue-1619 regression: custom-profile users got HTTP 500 because\n"+
			"    FlowInterview Insert was not elevated to SystemContext.", err)
	}
	if interview == nil {
		t.Fatal("(a) CreateInterview returned nil interview without error")
	}
	if interview.Id == "" {
		t.Fatal("(a) CreateInterview returned interview with empty Id")
	}
	t.Logf("(a) CreateInterview SUCCEEDED; interview.Id=%s", interview.Id)

	// Verify OwnerId attribution is preserved (elevation must not change ownership).
	var dbOwnerIdAfterCreate string
	if err := db.QueryRowContext(sysCtx,
		`SELECT COALESCE(OwnerId, '') FROM FlowInterview WHERE Id = ? LIMIT 1`,
		interview.Id,
	).Scan(&dbOwnerIdAfterCreate); err != nil {
		t.Fatalf("query FlowInterview row after CreateInterview: %v", err)
	}
	if dbOwnerIdAfterCreate != customUserID {
		t.Fatalf("FlowInterview.OwnerId = %q after CreateInterview, want %q — elevation must not alter record ownership", dbOwnerIdAfterCreate, customUserID)
	}
	t.Logf("(a) FlowInterview.OwnerId=%s (correct — elevation does not alter attribution)", dbOwnerIdAfterCreate)

	// ---------------------------------------------------------------
	// (b) CompleteInterview(userCtx) must FAIL for the custom-profile user.
	//     CompleteInterview is now bare in-method; a caller without
	//     FlowInterview edit permission is denied. This proves the resume
	//     path is NOT escalated (the Codex BLOCK finding).
	//     Run (b) BEFORE (c) so the interview is still Running.
	// ---------------------------------------------------------------
	bareErr := sm.FlowInterviewSvc.CompleteInterview(userCtx, interview.Id, models.FlowInterviewStatusFinished, customUser)
	if bareErr == nil {
		t.Fatal("(b) CompleteInterview(userCtx) succeeded — expected permission-denied error for custom-profile user without FlowInterview write permission.\n" +
			"  → The user-facing resumeFlow path must NOT be elevated.")
	}
	t.Logf("(b) CompleteInterview(userCtx) correctly returned error: %v", bareErr)

	// ---------------------------------------------------------------
	// (c) CompleteInterview(elevatedCtx) must SUCCEED.
	//     This mirrors what the fresh-execution call sites do — they pass
	//     processTableWriteContext(ctx, ...) which wraps WithSecurityContext.
	//     The test package cannot call the private processTableWriteContext
	//     helper, so use WithSecurityContext directly (same effect).
	// ---------------------------------------------------------------
	elevatedCtx := constants.WithSecurityContext(userCtx, constants.SystemContext, "test: fresh-exec bookkeeping")
	if err := sm.FlowInterviewSvc.CompleteInterview(elevatedCtx, interview.Id, models.FlowInterviewStatusFinished, customUser); err != nil {
		t.Fatalf("(c) CompleteInterview(elevatedCtx) failed: %v\n"+
			"  → The internal fresh-execution path must succeed with elevated ctx.", err)
	}
	t.Log("(c) CompleteInterview(elevatedCtx) SUCCEEDED")

	// ---------------------------------------------------------------
	// Verify the row still exists in DB and OwnerId attribution is preserved
	// (elevation must not change the ownership attribution).
	// ---------------------------------------------------------------
	var dbOwnerId string
	if err := db.QueryRowContext(sysCtx,
		`SELECT COALESCE(OwnerId, '') FROM FlowInterview WHERE Id = ? LIMIT 1`,
		interview.Id,
	).Scan(&dbOwnerId); err != nil {
		t.Fatalf("query FlowInterview row: %v", err)
	}
	if dbOwnerId != customUserID {
		t.Fatalf("FlowInterview.OwnerId = %q, want %q — elevation must not alter record ownership", dbOwnerId, customUserID)
	}
	t.Logf("FlowInterview.OwnerId=%s (correct — elevation does not alter attribution)", dbOwnerId)

	t.Log("=== TestFlowInterviewElevation_CustomProfileCanWriteWithoutPermission PASSED ===")
}
