package automation_test

// Tests for the approval-submit lock-probe fix: the LockRule must be evaluated
// against the raw (un-FLS-filtered) record, not eligibility.TargetRecord.
//
// Two unit tests cover the key behaviors:
//  1. GetRaw error → Submit returns internal error (fail-closed).
//  2. Raw record triggers LockRule even when eligibility.TargetRecord omits
//     the field that makes the formula fire.

import (
	"context"
	"errors"
	"fmt"
	"testing"

	automation "github.com/nexuscrm/automation"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/logging"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// lockTestPermRepo is a no-op ports.PermissionRepository used to construct a
// real *permissions.PermissionService without a database. GetAllRoles is the
// only wired method (NewPermissionService init reads it); the embedded
// interface traps any other call with a nil panic, so the formula-engine
// LockRule evaluation is the only meaningful signal under test.
type lockTestPermRepo struct {
	ports.PermissionRepository
}

func (r *lockTestPermRepo) GetAllRoles(context.Context) ([]*models.UserRole, error) {
	return nil, nil
}
func (r *lockTestPermRepo) GetPendingApprovalLockInfo(context.Context, string) (string, string, error) {
	return "", "", nil
}

var _ ports.PermissionRepository = (*lockTestPermRepo)(nil)

// lockTestSchemaLookup returns a fixed schema for a single object.
type lockTestSchemaLookup struct {
	apiName  string
	lockRule *string
}

func (s *lockTestSchemaLookup) GetSchema(_ context.Context, apiName string) *models.ObjectMetadata {
	if apiName == s.apiName {
		return &models.ObjectMetadata{
			APIName:      s.apiName,
			SharingModel: constants.SharingModelPublicReadWrite,
			LockRule:     s.lockRule,
		}
	}
	return nil
}

func lockRulePtr(expr string) *string { return &expr }

// lockTestPersistenceStub extends submitPersistenceStub with a controllable
// GetRaw error, used to verify fail-closed behavior when raw record is
// unavailable.
type lockTestPersistenceStub struct {
	submitPersistenceStub
}

func newSubmitTestServiceWithPermissions(
	persistence *lockTestPersistenceStub,
	query *submitQueryStub,
	permSvc *permissions.PermissionService,
	schema automation.SchemaLookup,
) *automation.ApprovalService {
	return automation.NewApprovalService(
		persistence,
		query,
		permSvc,
		nil,
		nil,
		nil,
		schema,
		logging.Default,
	)
}

// newLockPermissionService creates a real PermissionService backed by the
// no-op repo. It uses the formula engine embedded by NewPermissionService,
// which evaluates LockRule expressions.
func newLockPermissionService() *permissions.PermissionService {
	return permissions.NewPermissionService(&lockTestPermRepo{}, nil, nil)
}

// TestApprovalService_Submit_GetRawErrorFailsClosed verifies that when
// GetRaw returns an error during the lock probe, Submit denies the request
// rather than falling back to the FLS-filtered record and silently proceeding.
func TestApprovalService_Submit_GetRawErrorFailsClosed(t *testing.T) {
	persistence := &lockTestPersistenceStub{
		submitPersistenceStub: submitPersistenceStub{recordType: "Opportunity", getRawErr: errors.New("transient db error")},
	}
	query := &submitQueryStub{
		onQuery: func(ctx context.Context, objectName, filter string, user *models.UserSession, sortField, sortDir string, limit int) (*models.QueryResult, error) {
			switch objectName {
			case "Opportunity":
				return &models.QueryResult{
					TotalSize: 1,
					Records:   []models.SObject{{constants.FieldID: "opp-1", "StageName": "Closed Won"}},
				}, nil
			case constants.TableProcessDefinition:
				return &models.QueryResult{TotalSize: 1, Records: []models.SObject{{
					constants.FieldID: "pd-lock-1",
					"TableEnumOrId":   "Opportunity",
					"State":           "Active",
					"EntryFilter":     "",
					"Metadata":        "{}",
				}}}, nil
			default:
				return emptyResult(), nil
			}
		},
	}
	permSvc := newLockPermissionService()
	schema := &lockTestSchemaLookup{apiName: "Opportunity", lockRule: lockRulePtr("StageName == 'Closed Won'")}
	svc := newSubmitTestServiceWithPermissions(persistence, query, permSvc, schema)

	_, err := svc.Submit(context.Background(), automation.SubmitRequest{
		ObjectAPIName: "Opportunity",
		RecordID:      "opp-1",
	}, &models.UserSession{ID: "005-user", ProfileID: "standard"})
	if err == nil {
		t.Fatal("Submit() error = nil, want error when GetRaw fails")
	}
	if appErrors.IsPermission(err) {
		t.Fatalf("Submit() returned permission error, want internal error: %v", err)
	}
	if len(persistence.inserts) != 0 {
		t.Fatalf("Insert calls = %d, want 0 when lock-probe raw fetch fails", len(persistence.inserts))
	}
}

// TestApprovalService_Submit_LockProbeUsesRawRecord verifies that the lock
// probe evaluates the LockRule against the raw (un-FLS-filtered) record.
//
// Setup:
//   - eligibility.TargetRecord (from QueryWithFilter) omits InternalStatus__c.
//   - GetRaw returns the full record that includes InternalStatus__c = "Locked".
//   - LockRule: "InternalStatus__c == 'Locked'"
//
// Before the fix, Submit would evaluate the LockRule against the incomplete
// eligibility record (missing field → formula error → fail-open → IsLocked=false)
// and proceed with the submission. After the fix, the raw record is used, the
// formula evaluates to true, and Submit is denied.
func TestApprovalService_Submit_LockProbeUsesRawRecord(t *testing.T) {
	const recordID = "opp-raw-test"
	persistence := &lockTestPersistenceStub{
		submitPersistenceStub: submitPersistenceStub{
			recordType: "Opportunity",
			getRawRecords: map[string]models.SObject{
				// Full raw record: includes InternalStatus__c which is FLS-restricted.
				"Opportunity/" + recordID: {
					constants.FieldID:   recordID,
					"StageName":         "Prospecting",
					"InternalStatus__c": "Locked",
				},
			},
		},
	}
	query := &submitQueryStub{
		onQuery: func(ctx context.Context, objectName, filter string, user *models.UserSession, sortField, sortDir string, limit int) (*models.QueryResult, error) {
			switch objectName {
			case "Opportunity":
				// FLS-filtered projection: InternalStatus__c is absent.
				return &models.QueryResult{
					TotalSize: 1,
					Records:   []models.SObject{{constants.FieldID: recordID, "StageName": "Prospecting"}},
				}, nil
			case constants.TableProcessDefinition:
				return &models.QueryResult{TotalSize: 1, Records: []models.SObject{{
					constants.FieldID: "pd-raw-1",
					"TableEnumOrId":   "Opportunity",
					"State":           "Active",
					"EntryFilter":     "",
					"Metadata":        "{}",
				}}}, nil
			default:
				return emptyResult(), nil
			}
		},
	}
	permSvc := newLockPermissionService()
	// LockRule fires when InternalStatus__c == 'Locked' — a field absent from the
	// FLS-filtered projection but present in the raw record.
	schema := &lockTestSchemaLookup{
		apiName:  "Opportunity",
		lockRule: lockRulePtr("InternalStatus__c == 'Locked'"),
	}
	svc := newSubmitTestServiceWithPermissions(persistence, query, permSvc, schema)

	user := &models.UserSession{
		ID:        "005-regular",
		ProfileID: "standard",
		// Not a system admin — lock bypass does not apply.
	}
	_, err := svc.Submit(context.Background(), automation.SubmitRequest{
		ObjectAPIName: "Opportunity",
		RecordID:      recordID,
	}, user)

	if err == nil {
		t.Fatal("Submit() error = nil, want locked-record permission error")
	}
	if !appErrors.IsPermission(err) {
		t.Fatalf("Submit() error = %T %v, want permission error for locked record", err, err)
	}
	if len(persistence.inserts) != 0 {
		t.Fatalf("Insert calls = %d, want 0 for locked record", len(persistence.inserts))
	}

	// Confirm GetRaw was called — the lock probe must use the raw path.
	found := false
	for _, id := range persistence.getRawCalls {
		if id == recordID {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("GetRaw was not called for %q; lock probe must use raw record, not eligibility.TargetRecord. Calls: %v",
			recordID, persistence.getRawCalls)
	}
}

// Ensure the test stub satisfies the full PersistenceProvider interface so
// compile-time failures are caught before runtime.
var _ automation.PersistenceProvider = (*lockTestPersistenceStub)(nil)

// stringFormatCheck ensures no stray fmt import warning.
var _ = fmt.Sprintf
