package automation_test

import (
	"context"
	"fmt"
	"strings"
	"testing"

	automation "github.com/nexuscrm/automation"
	"github.com/nexuscrm/kernel/pkg/logging"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

// routingQueryStub extends actionQueryStub to return two sibling workitems
// and a ProcessDefinition with configurable approval metadata routing.
type routingQueryStub struct {
	whenMultipleApprovers string
	nextStepFailureAction string
	currentNodeID         string
	workItemActorID       string
	driftCurrentNode      bool
	pdQuerySeen           bool
	actionFilters         []string
}

func (s *routingQueryStub) QueryWithFilter(ctx context.Context, objectName, filter string, user *models.UserSession, sortField, sortDir string, limit int) (*models.QueryResult, error) {
	switch objectName {
	case constants.TableProcessInstanceWorkitem:
		if strings.Contains(filter, "Id == 'wi-1'") {
			return routingActionQueryResult(models.SObject{
				constants.FieldID:   "wi-1",
				"ProcessInstanceId": "pi-1",
				"ActorId":           s.workItemActorID,
				"OriginalActorId":   s.workItemActorID,
			}), nil
		}
		if strings.Contains(filter, "ProcessInstanceId == 'pi-1'") {
			// Return two sibling workitems for Unanimous mode testing
			return routingActionQueryResult(
				models.SObject{
					constants.FieldID:   "wi-1",
					"ProcessInstanceId": "pi-1",
					"ActorId":           s.workItemActorID,
					"OriginalActorId":   s.workItemActorID,
				},
				models.SObject{
					constants.FieldID:   "wi-2",
					"ProcessInstanceId": "pi-1",
					"ActorId":           "005-approver-2",
					"OriginalActorId":   "005-approver-2",
				},
			), nil
		}
	case constants.TableProcessInstance:
		if strings.Contains(filter, "Id == 'pi-1'") {
			return routingActionQueryResult(models.SObject{
				constants.FieldID:     "pi-1",
				"ProcessDefinitionId": "pd-1",
				"TargetObjectId":      "record-1",
				"SubmittedById":       "005-submitter",
				"Status":              constants.ApprovalStatusPending,
			}), nil
		}
	case constants.TableProcessDefinition:
		s.pdQuerySeen = true
		return routingActionQueryResult(models.SObject{
			constants.FieldID: "pd-1",
			"Name":            "Generic Approval",
			"DeveloperName":   "Generic_Approval",
			"TableEnumOrId":   "GenericRecord",
			"Metadata":        routingApprovalMetadata(s.whenMultipleApprovers, s.nextStepFailureAction),
		}), nil
	case constants.TableProcessInstanceNode:
		if strings.Contains(filter, "ProcessInstanceId == 'pi-1'") && strings.Contains(filter, "NodeStatus == 'Pending'") {
			return routingActionQueryResult(models.SObject{
				constants.FieldID:   "pin-current",
				"ProcessInstanceId": "pi-1",
				"ProcessNodeId":     s.currentNodeID,
				"ProcessNodeName":   "First Step",
				"NodeStatus":        constants.ApprovalStatusPending,
			}), nil
		}
	case constants.TableProcessNode:
		// loadProcessNode queries by Id for the current step
		if strings.Contains(filter, "Id == 'pn-1'") {
			if s.driftCurrentNode {
				return routingActionQueryResult(models.SObject{
					constants.FieldID:     "pn-1",
					"Name":                "Runtime Drift",
					"DeveloperName":       "Runtime_Drift",
					"ProcessDefinitionId": "pd-1",
					"StepOrder":           99,
					"ApproverType":        "user",
					"ApproverUserId":      s.workItemActorID,
					"AllowComments":       true,
					"RequireComments":     false,
					"RejectBehavior":      "FinalRejection",
				}), nil
			}
			return routingActionQueryResult(models.SObject{
				constants.FieldID:     "pn-1",
				"Name":                "First Step",
				"DeveloperName":       "First_Step",
				"ProcessDefinitionId": "pd-1",
				"StepOrder":           1,
				"ApproverType":        "user",
				"ApproverUserId":      s.workItemActorID,
				"AllowComments":       true,
				"RequireComments":     false,
				"RejectBehavior":      "FinalRejection",
			}), nil
		}
		// determineNextApprovalStep queries all nodes by ProcessDefinitionId
		if strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") {
			return routingActionQueryResult(
				models.SObject{
					constants.FieldID:     "pn-1",
					"Name":                "First Step",
					"DeveloperName":       "First_Step",
					"ProcessDefinitionId": "pd-1",
					"StepOrder":           1,
					"ApproverType":        "user",
					"ApproverUserId":      s.workItemActorID,
					"AllowComments":       true,
					"RequireComments":     false,
					"RejectBehavior":      "FinalRejection",
				},
				models.SObject{
					constants.FieldID:     "pn-2",
					"Name":                "Second Step",
					"DeveloperName":       "Second_Step",
					"ProcessDefinitionId": "pd-1",
					"StepOrder":           2,
					"ApproverType":        "user",
					"ApproverUserId":      "005-approver-2",
					"AllowComments":       true,
					"RequireComments":     false,
					"RejectBehavior":      "FinalRejection",
				},
			), nil
		}
	case constants.TableUser:
		id := "005-approver-1"
		if strings.Contains(filter, "005-approver-2") {
			id = "005-approver-2"
		}
		return routingActionQueryResult(models.SObject{
			constants.FieldID: id,
			"Username":        id + "@example.test",
		}), nil
	case "GenericRecord":
		return routingActionQueryResult(models.SObject{
			constants.FieldID: "record-1",
			"Status":          "Open",
		}), nil
	case constants.TableProcessAction:
		s.actionFilters = append(s.actionFilters, filter)
		return routingActionQueryResult(), nil
	}
	return routingActionQueryResult(), nil
}

func (s *routingQueryStub) Query(ctx context.Context, req api.QueryRequest, user *models.UserSession) (*models.QueryResult, error) {
	return routingActionQueryResult(), nil
}

func (s *routingQueryStub) QueryWithSpec(ctx context.Context, spec queryspec.QuerySpec, user *models.UserSession) (*models.QueryResult, error) {
	// Criteria evaluation reads through a field-capped SystemContext
	// QueryWithSpec (#3945); route it into the same object/filter switch the
	// legacy QueryWithFilter reads use, reconstructing the canonical
	// formula-expression form ("Id == '...'") for simple comparisons.
	filterExpr := ""
	if spec.Filter.Kind == queryspec.PredicateFieldComparison && spec.Filter.Op == "=" {
		filterExpr = fmt.Sprintf("%s == '%v'", spec.Filter.Field, spec.Filter.Value)
	} else {
		lowering, err := queryspec.LowerPredicate(spec.Filter, queryspec.PredicateLoweringExpressionOnly)
		if err != nil {
			return nil, err
		}
		filterExpr = lowering.Expression
	}
	return s.QueryWithFilter(ctx, spec.ObjectAPIName, filterExpr, user, spec.SortField, spec.SortDirection, spec.Limit)
}

func (s *routingQueryStub) RunAnalytics(ctx context.Context, q models.AnalyticsQuery, user *models.UserSession) (interface{}, error) {
	panic("routingQueryStub.RunAnalytics: not implemented")
}

func (s *routingQueryStub) SearchSingleObject(ctx context.Context, objectName, term string, searchFields, returnFields []string, limit, offset int, orderBy, orderDir, filterExpr string, user *models.UserSession) ([]models.SObject, error) {
	panic("routingQueryStub.SearchSingleObject: not implemented")
}

func (s *routingQueryStub) SearchSuggestions(context.Context, string, string, []string, []string, int, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func (s *routingQueryStub) GlobalSearch(ctx context.Context, term string, user *models.UserSession) ([]models.SearchResult, error) {
	panic("routingQueryStub.GlobalSearch: not implemented")
}

func (s *routingQueryStub) Calculate(ctx context.Context, objectName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	panic("routingQueryStub.Calculate: not implemented")
}

func (s *routingQueryStub) ExecuteRawSQL(ctx context.Context, sql string, params []interface{}, user *models.UserSession) ([]models.SObject, error) {
	panic("routingQueryStub.ExecuteRawSQL: not implemented")
}

var _ ports.QueryService = (*routingQueryStub)(nil)

func routingActionQueryResult(records ...models.SObject) *models.QueryResult {
	cloned := make([]models.SObject, 0, len(records))
	for _, record := range records {
		c := make(models.SObject)
		for k, v := range record {
			c[k] = v
		}
		cloned = append(cloned, c)
	}
	return &models.QueryResult{TotalSize: len(cloned), Records: cloned}
}

func routingApprovalMetadata(whenMultipleApprovers, nextStepFailureAction string) string {
	nextStepCriteria := ""
	if nextStepFailureAction != "" {
		nextStepCriteria = fmt.Sprintf(`,
			"entryCriteria": {
				"criteriaItems": {"field": "Status", "operation": "equals", "value": "Ready"}
			},
			"ifCriteriaNotMet": %q`, nextStepFailureAction)
	}
	return fmt.Sprintf(`{
		"approvalStep": [{
			"name": "First_Step",
			"label": "First Step",
			"assignedApprover": {
				"approver": [
					{ "type": "user", "name": "005-approver-1" },
					{ "type": "user", "name": "005-approver-2" }
				],
				"whenMultipleApprovers": %q
			}
		}, {
			"name": "Second_Step",
			"label": "Second Step",
			"assignedApprover": {
				"approver": [
					{ "type": "user", "name": "005-approver-2" }
				],
				"whenMultipleApprovers": "FirstResponse"
			}%s
		}]
	}`, whenMultipleApprovers, nextStepCriteria)
}

func newRoutingActionTestService(persistence *actionPersistenceStub, query *routingQueryStub) *automation.ApprovalService {
	return automation.NewApprovalService(
		persistence,
		query,
		nil,
		nil,
		nil,
		nil,
		nil,
		logging.Default,
	)
}

func routingActionUser(id string) *models.UserSession {
	return &models.UserSession{
		ID:        id,
		Name:      id,
		ProfileID: constants.ProfileStandardUser,
	}
}

// TestApprovalServiceRoutingBranch_FirstResponse verifies that metadata
// whenMultipleApprovers="FirstResponse" deletes all sibling workitems (F6).
func TestApprovalServiceRoutingBranch_FirstResponse(t *testing.T) {
	persistence := &actionPersistenceStub{}
	query := &routingQueryStub{
		whenMultipleApprovers: "FirstResponse",
		currentNodeID:         "pn-1",
		workItemActorID:       "005-approver-1",
	}
	svc := newRoutingActionTestService(persistence, query)

	result, err := svc.Approve(context.Background(), "wi-1", "looks good", routingActionUser("005-approver-1"))
	if err != nil {
		t.Fatalf("Approve() error = %v", err)
	}
	if got := result["InstanceStatus"]; got != constants.ApprovalStatusPending {
		t.Fatalf("InstanceStatus = %v, want %s", got, constants.ApprovalStatusPending)
	}
	if !query.pdQuerySeen {
		t.Fatal("Approve() did not load ProcessDefinition")
	}

	// FirstResponse: delete both wi-1 and wi-2
	wantDeletes := map[string]bool{"wi-1": true, "wi-2": true}
	for _, del := range persistence.deletes {
		if del.tableName == constants.TableProcessInstanceWorkitem {
			delete(wantDeletes, del.id)
		}
	}
	if len(wantDeletes) > 0 {
		t.Errorf("missing workitem deletes: %v (got deletes: %+v)", wantDeletes, persistence.deletes)
	}

	// Next step should be inserted
	if !hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("inserts = %#v, want next ProcessInstanceNode for pn-2", persistence.inserts)
	}
}

func TestApprovalServiceRoutingBranch_RejectRecordTerminatesRejectedAfterApprovedStep(t *testing.T) {
	persistence := &actionPersistenceStub{}
	query := &routingQueryStub{
		whenMultipleApprovers: "FirstResponse",
		nextStepFailureAction: "rejectrecord",
		currentNodeID:         "pn-1",
		workItemActorID:       "005-approver-1",
	}
	svc := newRoutingActionTestService(persistence, query)

	result, err := svc.Approve(context.Background(), "wi-1", "looks good", routingActionUser("005-approver-1"))
	if err != nil {
		t.Fatalf("Approve() error = %v", err)
	}
	if got := result["InstanceStatus"]; got != constants.ApprovalStatusRejected {
		t.Fatalf("InstanceStatus = %v, want %s", got, constants.ApprovalStatusRejected)
	}
	if !hasUpdateStatus(persistence.updates, constants.TableProcessInstanceNode, "pin-current", "NodeStatus", constants.ApprovalStatusApproved) {
		t.Fatalf("updates = %#v, want current node Approved", persistence.updates)
	}
	if !hasInsertValue(persistence.inserts, constants.TableProcessInstanceHistory, "StepStatus", constants.ApprovalStatusApproved) {
		t.Fatalf("inserts = %#v, want current-step Approved history", persistence.inserts)
	}
	if !hasUpdateStatus(persistence.updates, constants.TableProcessInstance, "pi-1", "Status", constants.ApprovalStatusRejected) {
		t.Fatalf("updates = %#v, want terminal ProcessInstance Rejected", persistence.updates)
	}
	if hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") || hasInsertValue(persistence.inserts, constants.TableProcessInstanceWorkitem, "ProcessInstanceId", "pi-1") {
		t.Fatalf("inserts = %#v, want no next node or workitem", persistence.inserts)
	}
	actionFilters := strings.Join(query.actionFilters, "\n")
	if !strings.Contains(actionFilters, "ActionEvent == 'FinalRejection'") || strings.Contains(actionFilters, "ActionEvent == 'FinalApproval'") {
		t.Fatalf("ProcessAction filters = %q, want FinalRejection only", actionFilters)
	}
}

// TestApprovalServiceRoutingBranch_Unanimous verifies that metadata
// whenMultipleApprovers="Unanimous" keeps the step pending until all sibling
// approvers respond (F6).
func TestApprovalServiceRoutingBranch_Unanimous(t *testing.T) {
	persistence := &actionPersistenceStub{}
	query := &routingQueryStub{
		whenMultipleApprovers: "Unanimous",
		currentNodeID:         "pn-1",
		workItemActorID:       "005-approver-1",
	}
	svc := newRoutingActionTestService(persistence, query)

	result, err := svc.Approve(context.Background(), "wi-1", "looks good", routingActionUser("005-approver-1"))
	if err != nil {
		t.Fatalf("Approve() error = %v", err)
	}
	// Step should still be pending (not advanced)
	if got := result["InstanceStatus"]; got != constants.ApprovalStatusPending {
		t.Fatalf("InstanceStatus = %v, want %s", got, constants.ApprovalStatusPending)
	}

	// Unanimous: only delete wi-1, leave wi-2
	var deletedIDs []string
	for _, del := range persistence.deletes {
		if del.tableName == constants.TableProcessInstanceWorkitem {
			deletedIDs = append(deletedIDs, del.id)
		}
	}
	if len(deletedIDs) != 1 || deletedIDs[0] != "wi-1" {
		t.Errorf("deletes = %v, want [wi-1]", deletedIDs)
	}

	// No next step insert
	if hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("next step inserted on Unanimous partial approval: %#v", persistence.inserts)
	}

	// Instance should NOT be finalized
	if hasUpdateStatus(persistence.updates, constants.TableProcessInstance, "pi-1", "Status", constants.ApprovalStatusApproved) {
		t.Fatalf("ProcessInstance was finalized on Unanimous partial approval: %#v", persistence.updates)
	}
	if !hasInsertValue(persistence.inserts, constants.TableProcessInstanceHistory, "Comments", "looks good") {
		t.Fatalf("inserts = %#v, want partial approval comment history row", persistence.inserts)
	}
	if !hasInsertValue(persistence.inserts, constants.TableProcessInstanceHistory, "StepStatus", constants.ApprovalStatusApproved) {
		t.Fatalf("inserts = %#v, want partial approval status history row", persistence.inserts)
	}
}

// TestApprovalServiceRoutingBranch_UnresolvedUnanimousMetadataKeepsStepPending
// verifies that a drifted runtime ProcessNode cannot silently downgrade an
// authored Unanimous metadata step into FirstResponse sibling deletion.
func TestApprovalServiceRoutingBranch_UnresolvedUnanimousMetadataKeepsStepPending(t *testing.T) {
	persistence := &actionPersistenceStub{}
	query := &routingQueryStub{
		whenMultipleApprovers: "Unanimous",
		currentNodeID:         "pn-1",
		workItemActorID:       "005-approver-1",
		driftCurrentNode:      true,
	}
	svc := newRoutingActionTestService(persistence, query)

	result, err := svc.Approve(context.Background(), "wi-1", "looks good", routingActionUser("005-approver-1"))
	if err != nil {
		t.Fatalf("Approve() error = %v", err)
	}
	if got := result["InstanceStatus"]; got != constants.ApprovalStatusPending {
		t.Fatalf("InstanceStatus = %v, want %s", got, constants.ApprovalStatusPending)
	}
	if !query.pdQuerySeen {
		t.Fatal("Approve() did not load ProcessDefinition")
	}

	if !hasDelete(persistence.deletes, constants.TableProcessInstanceWorkitem, "wi-1") {
		t.Fatalf("deletes = %v, want ProcessInstanceWorkitem wi-1", persistence.deletes)
	}
	if hasDelete(persistence.deletes, constants.TableProcessInstanceWorkitem, "wi-2") {
		t.Fatalf("deleted sibling workitem despite unresolved Unanimous metadata step: %#v", persistence.deletes)
	}
	if hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("next step inserted despite unresolved Unanimous metadata step: %#v", persistence.inserts)
	}
	if hasUpdateStatus(persistence.updates, constants.TableProcessInstance, "pi-1", "Status", constants.ApprovalStatusApproved) {
		t.Fatalf("ProcessInstance was finalized despite unresolved Unanimous metadata step: %#v", persistence.updates)
	}
}
