//go:build crm

package crm

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestExecuteChangeOwnerUsesStandardActionSemantics(t *testing.T) {
	records := newCRMChangeOwnerRecordsStub()
	records.put("Lead", "00Qold", contract.Record{"Id": "00Qold", "OwnerId": "user-1", "Name": "Acme"})
	records.put(constants.TableUser, "005new", contract.Record{"Id": "005new", constants.FieldIsActive: true})
	records.updateCheck = func(ctx context.Context) error {
		if !constants.IsScopedInternalAccess(ctx, "Lead", constants.PermEdit) {
			t.Fatal("non-owner transfer must stamp scoped edit access for the target object")
		}
		return nil
	}
	policy := &crmChangeOwnerPolicyStub{grants: map[contract.Capability]bool{
		contract.Capability("TransferAnyLead"): true,
	}}
	exec := automation.NewFlowExecutor(nil, nil, nil, nil, nil, nil, nil)
	exec.SetStandardActionContractDeps(contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Policy:                    policy,
	})
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	action := &models.FlowActionCall{
		ActionType: "changeOwner",
		InputParameters: []models.FlowActionParameter{
			{Name: "recordId", Value: crmChangeOwnerStringValue("00Qold")},
			{Name: "objectApiName", Value: crmChangeOwnerStringValue("Lead")},
			{Name: "newOwnerId", Value: crmChangeOwnerStringValue("005new")},
		},
		OutputParameters: []models.FlowActionParameter{
			{Name: "recordId", Value: crmChangeOwnerOutputTarget("outRecordId")},
			{Name: "previousOwnerId", Value: crmChangeOwnerOutputTarget("outPreviousOwnerId")},
			{Name: "newOwnerId", Value: crmChangeOwnerOutputTarget("outNewOwnerId")},
		},
		Connector: &models.FlowConnector{TargetReference: "next"},
	}

	next, err := executeChangeOwner(context.Background(), exec, interview, &models.FlowMetadata{}, action, &models.UserSession{ID: "005caller"})
	if err != nil {
		t.Fatalf("executeChangeOwner err = %v", err)
	}
	if next != "next" {
		t.Fatalf("next = %q, want next", next)
	}
	if got := records.records["Lead"]["00Qold"]["OwnerId"]; got != "005new" {
		t.Fatalf("OwnerId = %v, want 005new", got)
	}
	if got := interview.ContextData["outRecordId"]; got != "00Qold" {
		t.Fatalf("outRecordId = %v, want 00Qold", got)
	}
	if got := interview.ContextData["outPreviousOwnerId"]; got != "user-1" {
		t.Fatalf("outPreviousOwnerId = %v, want user-1", got)
	}
	if got := interview.ContextData["outNewOwnerId"]; got != "005new" {
		t.Fatalf("outNewOwnerId = %v, want 005new", got)
	}
	if got := policy.checkedCaps; len(got) != 2 || got[0] != constants.PermTransferAnyEntity || got[1] != contract.Capability("TransferAnyLead") {
		t.Fatalf("checked capabilities = %v, want [TransferAnyEntity TransferAnyLead]", got)
	}
}

func crmChangeOwnerStringValue(value string) map[string]interface{} {
	return map[string]interface{}{"stringValue": value}
}

func crmChangeOwnerOutputTarget(value string) map[string]interface{} {
	return map[string]interface{}{"elementReference": value}
}

type crmChangeOwnerRecordsStub struct {
	records     map[string]map[string]contract.Record
	updateCheck func(context.Context) error
}

func newCRMChangeOwnerRecordsStub() *crmChangeOwnerRecordsStub {
	return &crmChangeOwnerRecordsStub{records: map[string]map[string]contract.Record{}}
}

func (s *crmChangeOwnerRecordsStub) put(objectName, id string, record contract.Record) {
	if s.records[objectName] == nil {
		s.records[objectName] = map[string]contract.Record{}
	}
	s.records[objectName][id] = crmChangeOwnerCloneRecord(record)
}

func (s *crmChangeOwnerRecordsStub) Insert(context.Context, string, contract.Record, *models.UserSession) (contract.Record, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) Retrieve(_ context.Context, objectName string, id string, _ *models.UserSession) (contract.Record, error) {
	if byID := s.records[objectName]; byID != nil {
		if record := byID[id]; record != nil {
			return crmChangeOwnerCloneRecord(record), nil
		}
	}
	return nil, errors.New("record not found")
}

func (s *crmChangeOwnerRecordsStub) RetrieveMany(context.Context, string, []string, *models.UserSession) (map[string]contract.Record, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) Update(ctx context.Context, objectName string, id string, updates contract.Record, _ *models.UserSession) error {
	if s.updateCheck != nil {
		if err := s.updateCheck(ctx); err != nil {
			return err
		}
	}
	if s.records[objectName] == nil {
		s.records[objectName] = map[string]contract.Record{}
	}
	record := crmChangeOwnerCloneRecord(s.records[objectName][id])
	if record == nil {
		record = contract.Record{"Id": id}
	}
	for key, value := range updates {
		record[key] = value
	}
	s.records[objectName][id] = record
	return nil
}

func (s *crmChangeOwnerRecordsStub) Delete(context.Context, string, string, *models.UserSession) error {
	return nil
}

func (s *crmChangeOwnerRecordsStub) UpsertByExternalID(context.Context, string, string, string, contract.Record, *models.UserSession) (contract.Record, bool, error) {
	return nil, false, nil
}

func (s *crmChangeOwnerRecordsStub) RetrieveByField(context.Context, string, string, string, *models.UserSession) (contract.Record, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) CreateRecords(context.Context, []contract.Record, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) UpdateRecords(context.Context, []contract.Record, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) DeleteRecords(context.Context, []string, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) UpsertRecords(context.Context, string, string, []contract.Record, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) RetrieveRecords(context.Context, string, []string, []string, *models.UserSession) ([]contract.Record, error) {
	return nil, nil
}

func (s *crmChangeOwnerRecordsStub) ObjectTypeForID(context.Context, string) (string, error) {
	return "", nil
}

func (s *crmChangeOwnerRecordsStub) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func (s *crmChangeOwnerRecordsStub) RequiresSourceAwareTransaction(string) bool {
	return false
}

type crmChangeOwnerPolicyStub struct {
	grants      map[contract.Capability]bool
	checkedCaps []contract.Capability
}

func (s *crmChangeOwnerPolicyStub) Access(context.Context, *models.UserSession, string, contract.AccessRequest) (*contract.EffectiveObjectAccess, error) {
	return nil, nil
}

func (s *crmChangeOwnerPolicyStub) HasCapability(_ context.Context, _ *models.UserSession, capability contract.Capability) bool {
	s.checkedCaps = append(s.checkedCaps, capability)
	return s.grants[capability]
}

func crmChangeOwnerCloneRecord(src contract.Record) contract.Record {
	if src == nil {
		return nil
	}
	dst := make(contract.Record, len(src))
	for key, value := range src {
		dst[key] = value
	}
	return dst
}
