package automation_test

import (
	"bytes"
	"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"
	"github.com/rs/zerolog"
)

// gateQueryStub drives the real Approve transition (determineNextApprovalStep →
// stepCriteriaMatches) through a two-step definition whose second step gates on
// a plain record field with ifCriteriaNotMet=ApproveRecord. It exists to pin
// the NX#3997 regression class from BOTH sides through the runtime path:
// a below-threshold record must SKIP the gated step and finalize Approved, and
// an above-threshold record must STOP at it. An unconditional-match regression
// (step metadata not found → silent early-true) flips the below-threshold case;
// an unconditional-skip regression flips the above-threshold case.
type gateQueryStub struct {
	// metadataValue is served as ProcessDefinition.Metadata verbatim. Tests use
	// string and []byte to pin that both driver representations decode
	// identically (the NX#3997 primary-suspect class: a swallowed .(string)
	// assertion turning []byte metadata into "no metadata").
	metadataValue interface{}
	recordAmount  interface{}
}

const gateApprovalMetadata = `{
	"approvalStep": [{
		"name": "First_Step",
		"label": "First Step",
		"assignedApprover": {
			"approver": [{ "type": "user", "name": "005-approver-1" }],
			"whenMultipleApprovers": "FirstResponse"
		}
	}, {
		"name": "Second_Step",
		"label": "Second Step",
		"assignedApprover": {
			"approver": [{ "type": "user", "name": "005-approver-2" }],
			"whenMultipleApprovers": "FirstResponse"
		},
		"entryCriteria": {
			"criteriaItems": {"field": "GenericRecord.Amount", "operation": "greaterOrEqual", "value": "90"}
		},
		"ifCriteriaNotMet": "ApproveRecord"
	}]
}`

func (s *gateQueryStub) 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":           "005-approver-1",
				"OriginalActorId":   "005-approver-1",
			}), nil
		}
		if strings.Contains(filter, "ProcessInstanceId == 'pi-1'") {
			return routingActionQueryResult(models.SObject{
				constants.FieldID:   "wi-1",
				"ProcessInstanceId": "pi-1",
				"ActorId":           "005-approver-1",
				"OriginalActorId":   "005-approver-1",
			}), 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:
		return routingActionQueryResult(models.SObject{
			constants.FieldID: "pd-1",
			"Name":            "Gate Approval",
			"DeveloperName":   "Gate_Approval",
			"TableEnumOrId":   "GenericRecord",
			"Metadata":        s.metadataValue,
		}), 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":     "pn-1",
				"ProcessNodeName":   "First Step",
				"NodeStatus":        constants.ApprovalStatusPending,
			}), nil
		}
	case constants.TableProcessNode:
		if strings.Contains(filter, "Id == 'pn-1'") {
			return routingActionQueryResult(gateProcessNode("pn-1", "First Step", "First_Step", 1, "005-approver-1")), nil
		}
		if strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") {
			return routingActionQueryResult(
				gateProcessNode("pn-1", "First Step", "First_Step", 1, "005-approver-1"),
				gateProcessNode("pn-2", "Second Step", "Second_Step", 2, "005-approver-2"),
			), 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",
			"Amount":          s.recordAmount,
		}), nil
	}
	return routingActionQueryResult(), nil
}

func gateProcessNode(id, name, developerName string, stepOrder int, approverID string) models.SObject {
	return models.SObject{
		constants.FieldID:     id,
		"Name":                name,
		"DeveloperName":       developerName,
		"ProcessDefinitionId": "pd-1",
		"StepOrder":           stepOrder,
		"ApproverType":        "user",
		"ApproverUserId":      approverID,
		"AllowComments":       true,
		"RequireComments":     false,
		"RejectBehavior":      "FinalRejection",
	}
}

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

func (s *gateQueryStub) 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 *gateQueryStub) RunAnalytics(ctx context.Context, q models.AnalyticsQuery, user *models.UserSession) (interface{}, error) {
	panic("gateQueryStub.RunAnalytics: not implemented")
}

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

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

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

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

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

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

func newGateTestService(persistence *actionPersistenceStub, query *gateQueryStub, logger *logging.Logger) *automation.ApprovalService {
	return automation.NewApprovalService(
		persistence,
		query,
		nil,
		nil,
		nil,
		nil,
		nil,
		logger,
	)
}

func approveGateStepOne(t *testing.T, metadataValue interface{}, amount interface{}, logger *logging.Logger) (*actionPersistenceStub, map[string]interface{}) {
	t.Helper()
	persistence := &actionPersistenceStub{}
	query := &gateQueryStub{metadataValue: metadataValue, recordAmount: amount}
	svc := newGateTestService(persistence, query, logger)

	result, err := svc.Approve(context.Background(), "wi-1", "gate check", routingActionUser("005-approver-1"))
	if err != nil {
		t.Fatalf("Approve() error = %v", err)
	}
	return persistence, result
}

// TestApprovalStepCriteriaGate_BelowThresholdSkipsWithApproveRecord pins the
// skip side: 30 < 90 → criteria do not match → ifCriteriaNotMet=ApproveRecord
// finalizes the instance without instantiating the gated step.
func TestApprovalStepCriteriaGate_BelowThresholdSkipsWithApproveRecord(t *testing.T) {
	persistence, result := approveGateStepOne(t, gateApprovalMetadata, 30, logging.Default)

	if got := result["InstanceStatus"]; got != constants.ApprovalStatusApproved {
		t.Fatalf("InstanceStatus = %v, want %s (below-threshold record must skip the gated step)", got, constants.ApprovalStatusApproved)
	}
	if hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("inserts = %#v, want NO gated-step node for a below-threshold record (unconditional-match regression, NX#3997)", persistence.inserts)
	}
	if !hasUpdateStatus(persistence.updates, constants.TableProcessInstance, "pi-1", "Status", constants.ApprovalStatusApproved) {
		t.Fatalf("updates = %#v, want terminal ProcessInstance Approved", persistence.updates)
	}
}

// TestApprovalStepCriteriaGate_AboveThresholdStops pins the stop side: 92 >= 90
// → criteria match → the gated step is instantiated and the instance stays
// Pending.
func TestApprovalStepCriteriaGate_AboveThresholdStops(t *testing.T) {
	persistence, result := approveGateStepOne(t, gateApprovalMetadata, 92, logging.Default)

	if got := result["InstanceStatus"]; got != constants.ApprovalStatusPending {
		t.Fatalf("InstanceStatus = %v, want %s (above-threshold record must stop at the gated step)", got, constants.ApprovalStatusPending)
	}
	if !hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("inserts = %#v, want gated-step node pn-2 for an above-threshold record", persistence.inserts)
	}
	if hasUpdateStatus(persistence.updates, constants.TableProcessInstance, "pi-1", "Status", constants.ApprovalStatusApproved) {
		t.Fatalf("updates = %#v, above-threshold record must not finalize Approved", persistence.updates)
	}
}

// TestApprovalStepCriteriaGate_MetadataBytesEquivalentToString pins that a
// []byte Metadata value routes identically to string. Before the
// approvalStringValue normalization, `def["Metadata"].(string)` silently
// yielded "" for []byte, decoded to nil metadata, and every gated step matched
// unconditionally (the NX#3997 primary-suspect class).
func TestApprovalStepCriteriaGate_MetadataBytesEquivalentToString(t *testing.T) {
	persistence, result := approveGateStepOne(t, []byte(gateApprovalMetadata), 30, logging.Default)

	if got := result["InstanceStatus"]; got != constants.ApprovalStatusApproved {
		t.Fatalf("InstanceStatus = %v, want %s ([]byte metadata must decode like string)", got, constants.ApprovalStatusApproved)
	}
	if hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("inserts = %#v, []byte metadata silently decoded as empty → unconditional match (NX#3997)", persistence.inserts)
	}
}

// TestApprovalStepCriteriaGate_MissingStepMetadataWarns pins the loudness
// guard: when the stored definition has no decodable metadata for a node, the
// legacy treat-as-match behavior is preserved but now emits a WARN naming the
// definition and node, so the silent early-true branch can no longer absorb a
// step-resolution regression invisibly.
func TestApprovalStepCriteriaGate_MissingStepMetadataWarns(t *testing.T) {
	var buf bytes.Buffer
	logger := logging.New(zerolog.New(&buf).WithContext(context.Background()))

	persistence, result := approveGateStepOne(t, "", 30, logger)

	// Legacy behavior: no metadata → every step treated as matched → the next
	// node becomes a real stop.
	if got := result["InstanceStatus"]; got != constants.ApprovalStatusPending {
		t.Fatalf("InstanceStatus = %v, want %s (legacy treat-as-match keeps the next step)", got, constants.ApprovalStatusPending)
	}
	if !hasInsertValue(persistence.inserts, constants.TableProcessInstanceNode, "ProcessNodeId", "pn-2") {
		t.Fatalf("inserts = %#v, want next node under legacy treat-as-match", persistence.inserts)
	}

	logged := buf.String()
	if !strings.Contains(logged, "stepCriteria:") || !strings.Contains(logged, "pd-1") || !strings.Contains(logged, "no decodable approval metadata") {
		t.Fatalf("log output = %q, want WARN naming ProcessDefinition pd-1 with no decodable metadata", logged)
	}
}
