package dialectsfcrm

import (
	"context"
	"errors"
	"testing"

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

func TestApprovalProcessOwnerlessLifecycleDelegatesToCanonicalExecutor(t *testing.T) {
	for _, test := range []struct {
		name, userID string
		err          error
		wantDiag     bool
	}{
		{name: "resolved", userID: "005U"},
		{name: "missing", wantDiag: true},
		{name: "lookup error", userID: "ignored", err: errors.New("lookup failed"), wantDiag: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			store, rawMetadata := approvalProcessOwnerlessLifecycleStoreForTest(t)
			store.userID, store.resolveErr = test.userID, test.err
			user := &models.UserSession{}
			var diagnostics []contract.RecordLifecycleDiagnostic
			ctx := contract.WithRecordLifecycleDiagnosticReporter(context.Background(), func(d contract.RecordLifecycleDiagnostic) {
				diagnostics = append(diagnostics, d)
			})
			hook, err := NewApprovalProcessOwnerlessLifecycleHook(store)
			if err != nil {
				t.Fatal(err)
			}
			if err = hook(ctx, approvalProcessOwnerlessLifecycleEvent(rawMetadata, user)); err != nil {
				t.Fatal(err)
			}
			if len(store.events) != 5 || store.events[0] != "lock:ProcessDefinition" || store.events[1] != "lock:ProcessNode" || store.events[2] != "lock:ProcessAction" || store.locks[0].Equal["Id"] != "definition-1" {
				t.Fatalf("canonical executor events = %#v, locks = %#v", store.events, store.locks)
			}
			if len(store.inserts) != 0 || len(store.updates) != 1 || store.updates[0].Table != approvalProcessNodeTable || len(store.deletes) != 1 || store.deletes[0].Table != approvalProcessActionTable {
				t.Fatalf("child reconciliation = inserts %#v, updates %#v, deletes %#v", store.inserts, store.updates, store.deletes)
			}
			if len(store.references) != 1 || store.references[0] != "first@example.com" || len(store.users) != 1 || store.users[0] != user {
				t.Fatalf("user resolution = references %#v, users %#v", store.references, store.users)
			}
			wantUserID := any(test.userID)
			if test.wantDiag {
				wantUserID = nil
			}
			if store.updates[0].Values["ApproverUserId"] != wantUserID {
				t.Fatalf("ApproverUserId = %#v, want %#v", store.updates[0].Values["ApproverUserId"], wantUserID)
			}
			if test.wantDiag {
				if len(diagnostics) != 1 || diagnostics[0].Problem != "Could not resolve approver user 'first@example.com' — stored without UserId" || diagnostics[0].ComponentType != "ApprovalProcess" {
					t.Fatalf("diagnostics = %#v", diagnostics)
				}
			} else if len(diagnostics) != 0 {
				t.Fatalf("diagnostics = %#v", diagnostics)
			}
		})
	}
}

func TestApprovalProcessOwnerlessLifecycleSkipsWithoutAccess(t *testing.T) {
	for name, mutate := range map[string]func(*contract.RecordLifecycleEvent){
		"missing metadata": func(event *contract.RecordLifecycleEvent) {
			delete(event.Record, constants.FieldProcessDefinition_Metadata)
		},
		"non-string metadata": func(event *contract.RecordLifecycleEvent) {
			event.Record[constants.FieldProcessDefinition_Metadata] = true
		},
		"blank metadata": func(event *contract.RecordLifecycleEvent) {
			event.Record[constants.FieldProcessDefinition_Metadata] = " "
		},
		"byte-identical metadata": func(event *contract.RecordLifecycleEvent) {
			event.OldRecord[constants.FieldProcessDefinition_Metadata] = event.Record[constants.FieldProcessDefinition_Metadata]
		},
	} {
		t.Run(name, func(t *testing.T) {
			store, rawMetadata := approvalProcessOwnerlessLifecycleStoreForTest(t)
			hook, err := NewApprovalProcessOwnerlessLifecycleHook(store)
			if err != nil {
				t.Fatal(err)
			}
			event := approvalProcessOwnerlessLifecycleEvent(rawMetadata, nil)
			mutate(&event)
			if err = hook(context.Background(), event); err != nil || len(store.events) != 0 || len(store.references) != 0 {
				t.Fatalf("error/events/references = %v/%#v/%#v", err, store.events, store.references)
			}
		})
	}
}

func TestApprovalProcessOwnerlessLifecycleRejectsIdentityWithoutAccess(t *testing.T) {
	if _, err := NewApprovalProcessOwnerlessLifecycleHook(nil); err == nil {
		t.Fatal("nil store was accepted")
	}
	for name, mutate := range map[string]func(*contract.RecordLifecycleEvent){
		"wrong object": func(event *contract.RecordLifecycleEvent) { event.ObjectName = constants.TableProcessNode },
		"missing id":   func(event *contract.RecordLifecycleEvent) { event.RecordID = "" },
		"unclean id":   func(event *contract.RecordLifecycleEvent) { event.RecordID = " definition-1 " },
		"missing name": func(event *contract.RecordLifecycleEvent) { delete(event.Record, "DeveloperName") },
		"unclean name": func(event *contract.RecordLifecycleEvent) { event.Record["DeveloperName"] = " Quote_Approval " },
		"invalid object": func(event *contract.RecordLifecycleEvent) {
			event.Record["TableEnumOrId"] = "Bad Object"
		},
	} {
		t.Run(name, func(t *testing.T) {
			store, rawMetadata := approvalProcessOwnerlessLifecycleStoreForTest(t)
			hook, err := NewApprovalProcessOwnerlessLifecycleHook(store)
			if err != nil {
				t.Fatal(err)
			}
			event := approvalProcessOwnerlessLifecycleEvent(rawMetadata, nil)
			mutate(&event)
			if err = hook(context.Background(), event); err == nil || len(store.events) != 0 || len(store.references) != 0 {
				t.Fatalf("error/events/references = %v/%#v/%#v", err, store.events, store.references)
			}
		})
	}
}

type approvalProcessOwnerlessLifecycleTestStore struct {
	*approvalProcessApplyTestStore
	userID     string
	resolveErr error
	references []string
	users      []*models.UserSession
}

func (s *approvalProcessOwnerlessLifecycleTestStore) ResolveApprovalProcessUserID(_ context.Context, username string, user *models.UserSession) (string, error) {
	s.references = append(s.references, username)
	s.users = append(s.users, user)
	return s.userID, s.resolveErr
}

func approvalProcessOwnerlessLifecycleStoreForTest(t *testing.T) (*approvalProcessOwnerlessLifecycleTestStore, string) {
	t.Helper()
	plan := approvalProcessPreAppliedUserPlanForTest(t)
	return &approvalProcessOwnerlessLifecycleTestStore{approvalProcessApplyTestStore: approvalProcessPreAppliedStore(plan)}, plan.Parent.RawMetadata
}

func approvalProcessOwnerlessLifecycleEvent(rawMetadata string, user *models.UserSession) contract.RecordLifecycleEvent {
	return contract.RecordLifecycleEvent{
		ObjectName: constants.TableProcessDefinition,
		RecordID:   "definition-1",
		Record: contract.Record{
			"DeveloperName": rawApprovalProcessDeveloperName,
			"TableEnumOrId": "Quote",
			constants.FieldProcessDefinition_Metadata: rawMetadata,
		},
		OldRecord:   contract.Record{},
		CurrentUser: user,
	}
}

const rawApprovalProcessDeveloperName = "Quote_Approval"
