package dialectsfcrm

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/contract"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

func TestApprovalProcessManagedApplyReconcilesStableParentAndNodes(t *testing.T) {
	request := approvalProcessManagedRequestForTest(t)
	store := &approvalProcessApplyTestStore{
		rows: map[string][]cp.ComponentRow{
			approvalProcessDefinitionTable: {{"Id": "definition-1", "DeveloperName": "quote_approval", "OwnerPack": "sales", "TableEnumOrId": "quote"}},
			approvalProcessNodeTable:       {{"Id": "node-review", "DeveloperName": "review", "IsDeleted": true}, {"Id": "node-old", "DeveloperName": "Old", "IsDeleted": "0"}},
		},
		ids: []string{"action-flow", "action-clear"},
	}
	if err := applyApprovalProcessManagedComponent(context.Background(), store, request); err != nil {
		t.Fatal(err)
	}
	if len(store.locks) != 3 || store.locks[0].Equal["DeveloperName"] != "Quote_Approval" || store.locks[1].Equal["ProcessDefinitionId"] != "definition-1" || store.locks[2].Table != approvalProcessActionTable || store.locks[2].Equal["ProcessDefinitionId"] != "definition-1" {
		t.Fatalf("locks = %#v", store.locks)
	}
	if _, filtered := store.locks[1].Equal["IsDeleted"]; filtered {
		t.Fatalf("ProcessNode lock excluded legacy NULL IsDeleted rows: %#v", store.locks[1])
	}
	if len(store.updates) != 3 || store.updates[0].Match["Id"] != "definition-1" || store.updates[0].Values["Metadata"] != string(request.Payload) || store.updates[1].Match["Id"] != "node-review" || store.updates[2].Match["Id"] != "node-old" || store.updates[2].Values["IsDeleted"] != true {
		t.Fatalf("parent/node updates = %#v", store.updates)
	}
	for _, column := range []string{"ApproverType", "ApproverUserId", "ApproverRelatedField"} {
		if value, exists := store.updates[1].Values[column]; !exists || value != nil {
			t.Fatalf("node update %s = %#v, want explicit nil", column, value)
		}
	}
	if len(store.deletes) != 1 || store.deletes[0].Table != approvalProcessActionTable || store.deletes[0].Match["ProcessDefinitionId"] != "definition-1" {
		t.Fatalf("action deletes = %#v", store.deletes)
	}
	if len(store.inserts) != 2 {
		t.Fatalf("action inserts = %#v", store.inserts)
	}
	actions := map[string]cp.ComponentRow{}
	for _, insert := range store.inserts {
		actions[insert.Values["Name"].(string)] = insert.Values
	}
	if actions["Clear Note"]["ProcessNodeId"] != "node-review" || actions["Clear Note"]["FieldValue"] != "" {
		t.Fatalf("step action = %#v", actions["Clear Note"])
	}
	if actions["Notify Flow"]["ProcessNodeId"] != nil || actions["Notify Flow"]["FieldValue"] != nil || actions["Notify Flow"]["ApiName"] != "Approval_Notify" {
		t.Fatalf("process action = %#v", actions["Notify Flow"])
	}
}
func TestApprovalProcessManagedApplyCreatesAndFailsClosed(t *testing.T) {
	request := approvalProcessManagedRequestForTest(t)
	created := &approvalProcessApplyTestStore{rows: map[string][]cp.ComponentRow{}, ids: []string{"definition", "node-review", "action-flow", "action-clear"}}
	if err := applyApprovalProcessManagedComponent(context.Background(), created, request); err != nil || len(created.locks) != 3 || len(created.inserts) != 4 || created.inserts[0].Table != approvalProcessDefinitionTable || created.inserts[1].Values["Id"] != "node-review" || created.inserts[2].Table != approvalProcessActionTable || len(created.deletes) != 0 {
		t.Fatalf("create = inserts %#v, error %v", created.inserts, err)
	}
	for name, store := range map[string]*approvalProcessApplyTestStore{
		"owner":  {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "other", "TableEnumOrId": "Quote"}}}},
		"object": {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Order"}}}},
		"missing id": {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{
			"DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote",
		}}}},
		"noncanonical id": {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": " definition ", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}}}},
		"duplicate parent": {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {
			{"Id": "one", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"},
			{"Id": "two", "DeveloperName": "quote_approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"},
		}}},
		"blank node":        {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}}, approvalProcessNodeTable: {{"Id": "one", "DeveloperName": "  "}}}},
		"noncanonical node": {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}}, approvalProcessNodeTable: {{"Id": "one", "DeveloperName": " Review "}}}},
		"malformed node":    {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}}, approvalProcessNodeTable: {{"Id": "one", "DeveloperName": "Review step"}}}},
		"invalid tombstone": {rows: map[string][]cp.ComponentRow{approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}}, approvalProcessNodeTable: {{"Id": "one", "DeveloperName": "Review", "IsDeleted": "no"}}}},
		"duplicate node": {rows: map[string][]cp.ComponentRow{
			approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}},
			approvalProcessNodeTable:       {{"Id": "one", "DeveloperName": "Review"}, {"Id": "two", "DeveloperName": "review"}},
		}},
		"duplicate tombstones": {rows: map[string][]cp.ComponentRow{
			approvalProcessDefinitionTable: {{"Id": "definition", "DeveloperName": "Quote_Approval", "OwnerPack": "sales", "TableEnumOrId": "Quote"}},
			approvalProcessNodeTable:       {{"Id": "one", "DeveloperName": "Review", "IsDeleted": true}, {"Id": "two", "DeveloperName": "review", "IsDeleted": true}},
		}},
	} {
		t.Run(name, func(t *testing.T) {
			err := applyApprovalProcessManagedComponent(context.Background(), store, request)
			if err == nil || len(store.inserts)+len(store.updates)+len(store.deletes) != 0 {
				t.Fatalf("error/mutations = %v/%d", err, len(store.inserts)+len(store.updates)+len(store.deletes))
			}
		})
	}
	bad := request
	bad.PackName = " sales"
	store := &approvalProcessApplyTestStore{}
	if err := applyApprovalProcessManagedComponent(context.Background(), store, bad); err == nil || len(store.locks) != 0 || !strings.Contains(err.Error(), "not canonical") {
		t.Fatalf("decode-before-store = %v, locks %#v", err, store.locks)
	}
}

func TestApprovalProcessPersistencePlanPreAppliedUserResolution(t *testing.T) {
	plan := approvalProcessPreAppliedUserPlanForTest(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 := approvalProcessPreAppliedStore(plan)
			var references []string
			var diagnostics []contract.RecordLifecycleDiagnostic
			ctx := contract.WithRecordLifecycleDiagnosticReporter(context.Background(), func(d contract.RecordLifecycleDiagnostic) { diagnostics = append(diagnostics, d) })
			err := reconcileApprovalProcessPersistencePlan(ctx, store, plan, func(_ context.Context, reference string) (string, error) {
				references = append(references, reference)
				return test.userID, test.err
			})
			if err != nil {
				t.Fatal(err)
			}
			if len(references) != 1 || references[0] != "first@example.com" {
				t.Fatalf("resolved references = %#v", references)
			}
			if len(store.events) < 4 || store.events[0] != "lock:ProcessDefinition" || store.events[1] != "lock:ProcessNode" || store.events[2] != "lock:ProcessAction" || store.locks[0].Equal["Id"] != plan.PreAppliedParentID {
				t.Fatalf("operation order = %#v", store.events)
			}
			if len(store.updates) != 1 || store.updates[0].Table != approvalProcessNodeTable || len(store.inserts) != 0 || store.updates[0].Values["ApproverUserId"] != func() any {
				if test.wantDiag {
					return nil
				}
				return test.userID
			}() {
				t.Fatalf("child reconciliation = updates %#v inserts %#v", store.updates, store.inserts)
			}
			if len(diagnostics) != func() int {
				if test.wantDiag {
					return 1
				}
				return 0
			}() || test.wantDiag && !strings.Contains(diagnostics[0].Problem, "first@example.com") {
				t.Fatalf("diagnostics = %#v", diagnostics)
			}
		})
	}
}

func TestApprovalProcessPersistencePlanPreAppliedParentFailsClosed(t *testing.T) {
	plan := approvalProcessPreAppliedUserPlanForTest(t)
	for name, mutate := range map[string]func(cp.ComponentRow){
		"missing":  nil,
		"wrong id": func(row cp.ComponentRow) { row["Id"] = "other" },
		"owned":    func(row cp.ComponentRow) { row["OwnerPack"] = "sales" },
		"identity": func(row cp.ComponentRow) { row["DeveloperName"] = "Other" },
		"object":   func(row cp.ComponentRow) { row["TableEnumOrId"] = "Order" },
		"metadata": func(row cp.ComponentRow) { row["Metadata"] = plan.Parent.RawMetadata + " " },
	} {
		t.Run(name, func(t *testing.T) {
			store := approvalProcessPreAppliedStore(plan)
			if mutate == nil {
				store.rows[approvalProcessDefinitionTable] = nil
			} else {
				mutate(store.rows[approvalProcessDefinitionTable][0])
			}
			err := reconcileApprovalProcessPersistencePlan(context.Background(), store, plan, func(context.Context, string) (string, error) { return "005U", nil })
			if err == nil || len(store.inserts)+len(store.updates)+len(store.deletes) != 0 {
				t.Fatalf("error/mutations = %v/%d", err, len(store.inserts)+len(store.updates)+len(store.deletes))
			}
		})
	}
}

func TestApprovalProcessPersistencePlanRejectsBeforeStoreAccess(t *testing.T) {
	for name, invalidPlan := range map[string]bool{"invalid plan": true, "missing user resolver": false} {
		t.Run(name, func(t *testing.T) {
			plan := approvalProcessPreAppliedUserPlanForTest(t)
			if invalidPlan {
				plan.ReplaceProcessActionRows = false
			}
			store := &approvalProcessApplyTestStore{}
			if err := reconcileApprovalProcessPersistencePlan(context.Background(), store, plan, nil); err == nil || len(store.events) != 0 {
				t.Fatalf("error/store events = %v/%#v", err, store.events)
			}
		})
	}
}

func approvalProcessPreAppliedUserPlanForTest(t *testing.T) ApprovalProcessPersistencePlan {
	t.Helper()
	raw := `<ApprovalProcess><approvalStep><name>Review</name><assignedApprover><approver><type>User</type><name>first@example.com</name></approver><approver><type>User</type><name>second@example.com</name></approver></assignedApprover></approvalStep></ApprovalProcess>`
	metadata, err := DecodeApprovalProcessMetadata(raw)
	if err != nil {
		t.Fatal(err)
	}
	plan, err := PlanApprovalProcessPersistence(ApprovalProcessProjectionRequest{ObjectAPIName: "Quote", DeveloperName: "Quote_Approval", ParentDisposition: ApprovalProcessParentPreAppliedOwnerless, PreAppliedParentID: "definition-1", RawMetadata: raw, Metadata: metadata})
	if err != nil {
		t.Fatal(err)
	}
	return plan
}

func approvalProcessPreAppliedStore(plan ApprovalProcessPersistencePlan) *approvalProcessApplyTestStore {
	return &approvalProcessApplyTestStore{rows: map[string][]cp.ComponentRow{
		approvalProcessDefinitionTable: {{"Id": plan.PreAppliedParentID, "DeveloperName": plan.Parent.BusinessKey.DeveloperName, "OwnerPack": nil, "TableEnumOrId": plan.Parent.BusinessKey.ObjectAPIName, "Metadata": plan.Parent.RawMetadata}},
		approvalProcessNodeTable:       {{"Id": "node-review", "DeveloperName": "Review"}},
	}}
}

func TestApprovalProcessManagedRowDeletedNormalizesDriverValues(t *testing.T) {
	for name, test := range map[string]struct {
		value   any
		deleted bool
	}{"null": {}, "bool": {value: true, deleted: true}, "string zero": {value: "0"}, "string one": {value: "1", deleted: true}, "int zero": {value: int64(0)}, "int one": {value: int64(1), deleted: true}} {
		t.Run(name, func(t *testing.T) {
			deleted, err := approvalProcessManagedRowDeleted(cp.ComponentRow{"IsDeleted": test.value})
			if err != nil || deleted != test.deleted {
				t.Fatalf("deleted/error = %v/%v", deleted, err)
			}
		})
	}
}

type approvalProcessApplyTestStore struct {
	rows    map[string][]cp.ComponentRow
	ids     []string
	locks   []cp.ComponentRowQuery
	inserts []cp.ComponentInsert
	updates []cp.ComponentUpdate
	deletes []cp.ComponentDelete
	events  []string
}

func (*approvalProcessApplyTestStore) ReadRows(context.Context, cp.ComponentRowQuery) ([]cp.ComponentRow, error) {
	return nil, nil
}
func (s *approvalProcessApplyTestStore) LockRows(_ context.Context, query cp.ComponentRowQuery) ([]cp.ComponentRow, error) {
	s.locks = append(s.locks, query)
	s.events = append(s.events, "lock:"+query.Table)
	return s.rows[query.Table], nil
}
func (s *approvalProcessApplyTestStore) NewID() string { id := s.ids[0]; s.ids = s.ids[1:]; return id }
func (s *approvalProcessApplyTestStore) InsertRow(_ context.Context, value cp.ComponentInsert) error {
	s.inserts = append(s.inserts, value)
	s.events = append(s.events, "insert:"+value.Table)
	return nil
}
func (s *approvalProcessApplyTestStore) UpdateRows(_ context.Context, value cp.ComponentUpdate) (int64, error) {
	s.updates = append(s.updates, value)
	s.events = append(s.events, "update:"+value.Table)
	return 0, nil
}
func (s *approvalProcessApplyTestStore) DeleteRows(_ context.Context, value cp.ComponentDelete) (int64, error) {
	s.deletes = append(s.deletes, value)
	s.events = append(s.events, "delete:"+value.Table)
	return 0, nil
}
