package standardaction

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"fmt"
	"testing"

	"github.com/nexuscrm/automation"
	cruntimeports "github.com/nexuscrm/db/pkg/runtimeports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	seedrows "github.com/pingcap-inc/nexuscrm-contract/pkg/seedrows"
)

// fakeStandardActions is a test-local implementation of the
// cruntimeports.StandardActionReader port. W4 inverted the standard-action
// metadata reads (schema / field-defaults / input-guards / internal-access
// scopes / field-mapping source) onto this port; the production impl lives in
// internal/bootstrap, which this package cannot import (it is about to move to
// the sibling github.com/nexuscrm/automation module). These reads no longer hit
// s.db, so service-logic tests inject canned rows through this port.
type fakeStandardActions struct {
	schema             []seedrows.ActionSchemaRow
	fieldDefaults      []seedrows.ActionFieldDefaultRow
	guards             []seedrows.ActionInputGuardRow
	scopes             []seedrows.StandardActionInternalAccessScopeRow
	fieldMappingSource string
	err                error
}

var _ cruntimeports.StandardActionReader = fakeStandardActions{}

func (f fakeStandardActions) GetActionSchema(_ *sql.DB, _ string) ([]seedrows.ActionSchemaRow, error) {
	return f.schema, f.err
}

func (f fakeStandardActions) GetActionFieldDefaults(_ *sql.DB, _ string) ([]seedrows.ActionFieldDefaultRow, error) {
	return f.fieldDefaults, f.err
}

func (f fakeStandardActions) GetActionInputGuards(_ *sql.DB, _ string) ([]seedrows.ActionInputGuardRow, error) {
	return f.guards, f.err
}

func (f fakeStandardActions) GetStandardActionInternalAccessScopes(_ *sql.DB, _ string) ([]seedrows.StandardActionInternalAccessScopeRow, error) {
	return f.scopes, f.err
}

func (f fakeStandardActions) GetFieldMappingSourceForTarget(_ *sql.DB, _, _, _, _ string) (string, error) {
	return f.fieldMappingSource, f.err
}

type bulkRecordingStandardActions struct {
	schemasByActionID       map[string][]seedrows.ActionSchemaRow
	fieldDefaultsByActionID map[string][]seedrows.ActionFieldDefaultRow
	guardsByActionID        map[string][]seedrows.ActionInputGuardRow
	schemaIDs               []string
	fieldDefaultIDs         []string
	guardIDs                []string
	singleCalls             int
}

var _ cruntimeports.StandardActionReader = (*bulkRecordingStandardActions)(nil)
var _ standardActionDiscoveryBulkReader = (*bulkRecordingStandardActions)(nil)

func (r *bulkRecordingStandardActions) GetActionSchema(_ *sql.DB, _ string) ([]seedrows.ActionSchemaRow, error) {
	r.singleCalls++
	return nil, errors.New("single schema read should not be called")
}

func (r *bulkRecordingStandardActions) GetActionFieldDefaults(_ *sql.DB, _ string) ([]seedrows.ActionFieldDefaultRow, error) {
	r.singleCalls++
	return nil, errors.New("single field-default read should not be called")
}

func (r *bulkRecordingStandardActions) GetActionInputGuards(_ *sql.DB, _ string) ([]seedrows.ActionInputGuardRow, error) {
	r.singleCalls++
	return nil, errors.New("single guard read should not be called")
}

func (r *bulkRecordingStandardActions) GetStandardActionInternalAccessScopes(_ *sql.DB, _ string) ([]seedrows.StandardActionInternalAccessScopeRow, error) {
	return nil, nil
}

func (r *bulkRecordingStandardActions) GetFieldMappingSourceForTarget(_ *sql.DB, _, _, _, _ string) (string, error) {
	return "", nil
}

func (r *bulkRecordingStandardActions) GetActionSchemas(_ *sql.DB, actionIDs []string) (map[string][]seedrows.ActionSchemaRow, error) {
	r.schemaIDs = append([]string(nil), actionIDs...)
	return r.schemasByActionID, nil
}

func (r *bulkRecordingStandardActions) GetActionFieldDefaultsForActions(_ *sql.DB, actionIDs []string) (map[string][]seedrows.ActionFieldDefaultRow, error) {
	r.fieldDefaultIDs = append([]string(nil), actionIDs...)
	return r.fieldDefaultsByActionID, nil
}

func (r *bulkRecordingStandardActions) GetActionInputGuardsForActions(_ *sql.DB, actionIDs []string) (map[string][]seedrows.ActionInputGuardRow, error) {
	r.guardIDs = append([]string(nil), actionIDs...)
	return r.guardsByActionID, nil
}

type servicePerms map[string]bool

func (p servicePerms) HasPermission(_ context.Context, _ *models.UserSession, permissionAPIName string) bool {
	return p[permissionAPIName]
}

func (p servicePerms) CheckObjectPermissionWithUser(context.Context, string, string, *models.UserSession) bool {
	return false
}

type userScopedPerms map[string]bool

func (p userScopedPerms) HasPermission(_ context.Context, user *models.UserSession, _ string) bool {
	return user != nil && p[user.ID]
}

func (p userScopedPerms) CheckObjectPermissionWithUser(context.Context, string, string, *models.UserSession) bool {
	return false
}

func actionRow(active, deleted bool, perms string) []driver.Value {
	return []driver.Value{"sa_convertLead", "convertLead", "Std_ConvertLead", "Std_ConvertLead_Preview", perms, true, active, deleted}
}

func TestActionFromContractRefFailsClosedUnlessActive(t *testing.T) {
	for _, state := range []contract.StandardActionState{
		"",
		contract.StandardActionAbsent,
		contract.StandardActionDisabled,
	} {
		ref := &contract.StandardActionRef{
			State: state,
			Name:  "convertLead",
			Binding: &standardActionBinding{action: Action{
				ID:                  "sa_convertLead",
				Name:                "convertLead",
				FlowDeveloperName:   "Std_ConvertLead",
				RequiredPermissions: []string{"ConvertLeads"},
				IsActive:            true,
			}},
		}
		if action, err := actionFromContractRef(ref); err == nil {
			t.Fatalf("actionFromContractRef(%q) = %#v, nil; want fail-closed error", state, action)
		} else {
			var actionErr *Error
			if !errors.As(err, &actionErr) || !actionErr.Internal {
				t.Fatalf("actionFromContractRef(%q) error = %#v; want internal standard-action error", state, err)
			}
		}
	}
}

func TestActionFromContractRefFailsClosedWithoutOpaqueBinding(t *testing.T) {
	ref := &contract.StandardActionRef{State: contract.StandardActionActive, Name: "convertLead"}
	if action, err := actionFromContractRef(ref); err == nil {
		t.Fatalf("actionFromContractRef(active forged ref) = %#v, nil; want fail-closed error", action)
	} else {
		var actionErr *Error
		if !errors.As(err, &actionErr) || !actionErr.Internal {
			t.Fatalf("actionFromContractRef(active forged ref) error = %#v; want internal standard-action error", err)
		}
	}
}

func TestActionFromContractRefFailsClosedOnBindingNameMismatch(t *testing.T) {
	ref := contractRefFromAction(&Action{
		ID:                  "sa_createRecord",
		Name:                "createRecord",
		FlowDeveloperName:   "Std_CreateRecord",
		RequiredPermissions: []string{"CreateRecords"},
		IsActive:            true,
	})
	ref.Name = "convertLead"

	if action, err := actionFromContractRef(ref); err == nil {
		t.Fatalf("actionFromContractRef(name-mismatched ref) = %#v, nil; want fail-closed error", action)
	} else {
		var actionErr *Error
		if !errors.As(err, &actionErr) || !actionErr.Internal {
			t.Fatalf("actionFromContractRef(name-mismatched ref) error = %#v; want internal standard-action error", err)
		}
	}
}

func TestActionFromContractRefRecoversOpaqueBinding(t *testing.T) {
	ref := contractRefFromAction(&Action{
		ID:                       "sa_convertLead",
		Name:                     "convertLead",
		FlowDeveloperName:        "Std_ConvertLead",
		PreviewFlowDeveloperName: "Std_ConvertLead_OutputPreview",
		RequiredPermissions:      []string{"ConvertLeads"},
		IsActive:                 true,
	})
	action, err := actionFromContractRef(ref)
	if err != nil {
		t.Fatalf("actionFromContractRef(active bound ref) error = %v", err)
	}
	if action.Name != "convertLead" || action.FlowDeveloperName != "Std_ConvertLead" {
		t.Fatalf("action = %#v, want recovered registry binding", action)
	}
	action.RequiredPermissions[0] = "Mutated"
	again, err := actionFromContractRef(ref)
	if err != nil {
		t.Fatalf("actionFromContractRef(second recovery) error = %v", err)
	}
	if again.RequiredPermissions[0] != "ConvertLeads" {
		t.Fatalf("binding permissions were aliased: %#v", again.RequiredPermissions)
	}
}

func TestAuthorizeActionFailsClosed(t *testing.T) {
	user := &models.UserSession{ID: "005-test"}
	action := &Action{Name: "convertLead", RequiredPermissions: []string{"ConvertLeads"}}
	denied := NewService(nil, servicePerms{})
	for _, action := range []*Action{{Name: "convertLead"}, action} {
		if got := denied.AuthorizeAction(context.Background(), action, user); got == nil {
			t.Fatalf("AuthorizeAction(%#v) = nil, want fail closed", action)
		}
	}
	if got := NewService(nil, servicePerms{"ConvertLeads": true}).AuthorizeAction(context.Background(), action, user); got != nil {
		t.Fatalf("AuthorizeAction(granted) = %#v, want nil", got)
	}
}

func TestStandardActionDescriptorCarriesHeaderFallbackEligibility(t *testing.T) {
	desc := standardActionDescriptorFromListEntry(ListEntry{
		Name:                   "quietAction",
		Label:                  "Quiet Action",
		FlowDeveloperName:      "Std_Quiet",
		HeaderFallbackEligible: false,
	})
	if desc.HeaderFallbackEligible {
		t.Fatalf("HeaderFallbackEligible = true, want false")
	}

	desc = standardActionDescriptorFromListEntry(ListEntry{
		Name:                   "legacyAction",
		Label:                  "Legacy Action",
		FlowDeveloperName:      "Std_Legacy",
		HeaderFallbackEligible: true,
	})
	if !desc.HeaderFallbackEligible {
		t.Fatalf("HeaderFallbackEligible = false, want true")
	}
}

func TestStandardActionDescriptorPreservesCollectionShapeFromRuntimeRows(t *testing.T) {
	service := &Service{}
	schema, err := service.schemaContractFromRows(t.Context(), []seedrows.ActionSchemaRow{{
		Direction: "input", APIName: "attachments", DataType: "id", IsCollection: true,
	}}, nil, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	fields := schemaFieldsToContract(schema.Inputs)
	if len(fields) != 1 || !fields[0].IsCollection {
		t.Fatalf("collection shape was not preserved: %#v", fields)
	}
}

func assertStringSlice(t *testing.T, got, want []string, label string) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("%s = %#v, want %#v", label, got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("%s = %#v, want %#v", label, got, want)
		}
	}
}

// recordContextSchemaRow feeds the test-local StandardActionReader fake the
// same record-context input descriptor the _System_ActionSchema reader query
// used to return: an input-direction, hidden field whose target is the given
// field on Lead.
func recordContextSchemaRow(apiName, targetField string) seedrows.ActionSchemaRow {
	return seedrows.ActionSchemaRow{
		Direction:          "input",
		APIName:            apiName,
		DataType:           "id",
		Label:              apiName,
		UIMode:             "hidden",
		TargetObject:       "Lead",
		TargetFieldAPIName: targetField,
		IsRequired:         false,
		SortOrder:          1,
	}
}

// recordingStandardActions wraps fakeStandardActions to record when the guard
// load path is reached, so a test can assert ordering (e.g. derive-before-guard)
// without arranging database-backed reader queries.
type recordingStandardActions struct {
	fakeStandardActions
	onGuards func()
}

func (r *recordingStandardActions) GetActionInputGuards(db *sql.DB, id string) ([]seedrows.ActionInputGuardRow, error) {
	if r.onGuards != nil {
		r.onGuards()
	}
	return r.fakeStandardActions.GetActionInputGuards(db, id)
}

func TestPreviewOutputsScalarOmitsInternalScopesAndFiltersScalars(t *testing.T) {
	action := &Action{ID: "sa_convertLead", Name: "convertLead", PreviewFlowDeveloperName: "Std_ConvertLead_Preview"}
	input := OutputPreviewInput{RecordContextInput: "leadId", FlowInputs: map[string]interface{}{"leadId": "00Q-test"}}
	got, err := NewService(nil).PreviewOutputsScalar(context.Background(), action, input, nil,
		func(ctx context.Context, flowName string, inputs map[string]interface{}, user *models.UserSession) (*automation.FlowPreviewOutputsResult, error) {
			if flowName != "Std_ConvertLead_Preview" || inputs["leadId"] != "00Q-test" {
				t.Fatalf("flowName=%q inputs=%#v", flowName, inputs)
			}
			if !constants.StandardActionExecutionOwnsRootFlow(ctx, "Std_ConvertLead_Preview") {
				t.Fatal("output-preview context does not own preview root flow")
			}
			if constants.HasStandardActionInternalAccessScopes(ctx) {
				t.Fatal("output-preview context must not include internal access scopes")
			}
			return &automation.FlowPreviewOutputsResult{IsSuccess: true, OutputValues: map[string]interface{}{"ok": true, "record": map[string]interface{}{"Id": "701"}}}, nil
		})
	if err != nil {
		t.Fatalf("PreviewOutputsScalar() error = %v", err)
	}
	if got.OutputValues["ok"] != true {
		t.Fatalf("OutputValues = %#v, want scalar ok", got.OutputValues)
	}
	if _, ok := got.OutputValues["record"]; ok {
		t.Fatalf("record-shaped output must be filtered: %#v", got.OutputValues)
	}
}

func TestOutcomeFromError_DeadlineExceeded_RemainsInternalWithoutAtomicActionProof(t *testing.T) {
	outcome := outcomeFromError("convertLead", context.DeadlineExceeded)
	if outcome == nil {
		t.Fatal("outcomeFromError(DeadlineExceeded) = nil, want internal outcome")
	}
	if outcome.Kind != contract.StandardActionOutcomeInternal {
		t.Fatalf("outcome.Kind = %q, want %q", outcome.Kind, contract.StandardActionOutcomeInternal)
	}
	if outcome.Name != "convertLead" {
		t.Fatalf("outcome.Name = %q, want convertLead", outcome.Name)
	}
	if !errors.Is(outcome.Cause, context.DeadlineExceeded) {
		t.Fatalf("outcome.Cause = %v, want context.DeadlineExceeded", outcome.Cause)
	}
}

func TestOutcomeFromActionError_AtomicFlowDeadlineProof_MapsToTimeout(t *testing.T) {
	wrapped := fmt.Errorf("flow execution: %w", automation.ErrAtomicFlowDeadlineExceeded)
	outcome := outcomeFromActionError(context.Background(), &Action{Name: "convertLead"}, wrapped)
	if outcome == nil {
		t.Fatal("outcomeFromActionError(wrapped atomic Flow deadline proof) = nil, want timeout outcome")
	}
	if outcome.Kind != contract.StandardActionOutcomeTimeout {
		t.Fatalf("outcome.Kind = %q, want %q", outcome.Kind, contract.StandardActionOutcomeTimeout)
	}
}

func TestOutcomeFromActionError_PlainDeadlineExceeded_RemainsInternal(t *testing.T) {
	outcome := outcomeFromActionError(context.Background(), &Action{Name: "convertLead", IsAtomic: true}, context.DeadlineExceeded)
	if outcome == nil {
		t.Fatal("outcomeFromActionError(plain DeadlineExceeded) = nil, want internal outcome")
	}
	if outcome.Kind != contract.StandardActionOutcomeInternal {
		t.Fatalf("outcome.Kind = %q, want %q", outcome.Kind, contract.StandardActionOutcomeInternal)
	}
}

func TestOutcomeFromActionError_ExpiredContextWithoutFlowProof_RemainsInternal(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	outcome := outcomeFromActionError(ctx, &Action{Name: "convertLead", IsAtomic: true}, errors.New("driver replaced context error"))
	if outcome == nil {
		t.Fatal("outcomeFromActionError(expired ctx without Flow proof) = nil, want internal outcome")
	}
	if outcome.Kind != contract.StandardActionOutcomeInternal {
		t.Fatalf("outcome.Kind = %q, want %q", outcome.Kind, contract.StandardActionOutcomeInternal)
	}
}
