package automation

import (
	"context"
	"reflect"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestEnsureProcessNodesForDefinitionScopesQueryOnlyToProvenOwners(t *testing.T) {
	const provenOwner = "crm-pack"
	definition := models.SObject{
		constants.FieldID:                          "pd-1",
		constants.FieldProcessDefinition_Metadata:  `{"approvalStep":[{"name":"Manager","label":"Manager Review"}]}`,
		constants.FieldProcessDefinition_OwnerPack: provenOwner,
	}
	user := &models.UserSession{ID: "005-user"}

	t.Run("startup-proven owner is query-only", func(t *testing.T) {
		query := &captureQuery{response: &models.QueryResult{}}
		// Persistence is intentionally nil: a submit-time repair write would panic.
		service := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil, WithApprovalQueryOnlyOwnerPacks([]string{provenOwner}))
		got, err := service.ensureProcessNodesForDefinition(context.Background(), definition, user)
		const wantError = "internal error: active approval process definition has no materialized steps"
		if !apperrors.IsInternal(err) || err.Error() != wantError || got != nil {
			t.Fatalf("missing proven topology = (%#v, %T %v), want (nil, %q)", got, err, err, wantError)
		}
		wantCalls := []captureQueryCall{{
			objectName: constants.TableProcessNode,
			filter:     "ProcessDefinitionId == 'pd-1'",
			sortField:  constants.FieldProcessNode_StepOrder,
			sortDir:    "ASC",
			limit:      ApprovalProcessNodeReadLimit,
		}}
		if !reflect.DeepEqual(query.calls, wantCalls) {
			t.Fatalf("topology queries = %#v, want %#v", query.calls, wantCalls)
		}

		stepLess := cloneAutomationSObject(definition)
		stepLess[constants.FieldProcessDefinition_Metadata] = "{}"
		got, err = service.ensureProcessNodesForDefinition(context.Background(), stepLess, user)
		if err != nil || got != nil {
			t.Fatalf("step-less proven definition = (%#v, %v), want (nil, nil)", got, err)
		}

		liveNodes := []models.SObject{{constants.FieldID: "pn-1"}, {constants.FieldID: "pn-2"}}
		query.response = &models.QueryResult{TotalSize: len(liveNodes), Records: liveNodes}
		got, err = service.ensureProcessNodesForDefinition(context.Background(), definition, user)
		if err != nil || !reflect.DeepEqual(got, liveNodes) {
			t.Fatalf("live proven topology = (%#v, %v), want (%#v, nil)", got, err, liveNodes)
		}
	})

	for _, tt := range []struct {
		name  string
		owner string
	}{
		{name: "ownerless", owner: ""},
		{name: "tenant-owned", owner: "tenant-pack"},
		{name: "companion-owned", owner: "companion-pack"},
		{name: "noncanonical owner", owner: "CRM-PACK"},
	} {
		t.Run(tt.name+" retains compatibility repair", func(t *testing.T) {
			query := &captureQuery{response: &models.QueryResult{}}
			persistence := &processNodeRepairPersistence{}
			service := NewApprovalService(persistence, query, nil, nil, nil, nil, nil, nil, WithApprovalQueryOnlyOwnerPacks([]string{provenOwner}))
			unproven := cloneAutomationSObject(definition)
			unproven[constants.FieldProcessDefinition_OwnerPack] = tt.owner

			got, err := service.ensureProcessNodesForDefinition(context.Background(), unproven, user)
			if err != nil || len(got) != 1 || len(persistence.inserts) != 1 {
				t.Fatalf("unproven topology repair = (%#v, %v, inserts=%d), want one repaired node", got, err, len(persistence.inserts))
			}
			if persistence.inserts[0].tableName != constants.TableProcessNode {
				t.Fatalf("repair insert table = %q, want %q", persistence.inserts[0].tableName, constants.TableProcessNode)
			}
			if gotName := persistence.inserts[0].record[constants.FieldName]; gotName != "Manager" {
				t.Fatalf("repaired ProcessNode.Name = %#v, want stable developer name Manager", gotName)
			}
			if gotDeveloperName := persistence.inserts[0].record[constants.FieldProcessNode_DeveloperName]; gotDeveloperName != "Manager" {
				t.Fatalf("repaired ProcessNode.DeveloperName = %#v, want Manager", gotDeveloperName)
			}
		})
	}
}

// TestEnsureProcessNodesForDefinitionAcceptsBytesMetadata pins that a []byte
// Metadata value rebuilds nodes exactly like a string one. Before the
// approvalStringValue normalization, the swallowed `.(string)` emptiness guard
// silently classified []byte metadata as "no metadata" and skipped the repair
// (NX#3997 class).
func TestEnsureProcessNodesForDefinitionAcceptsBytesMetadata(t *testing.T) {
	definition := models.SObject{
		constants.FieldID:                          "pd-1",
		constants.FieldProcessDefinition_Metadata:  []byte(`{"approvalStep":[{"name":"Manager","label":"Manager Review"}]}`),
		constants.FieldProcessDefinition_OwnerPack: "",
	}
	user := &models.UserSession{ID: "005-user"}

	query := &captureQuery{response: &models.QueryResult{}}
	persistence := &processNodeRepairPersistence{}
	service := NewApprovalService(persistence, query, nil, nil, nil, nil, nil, nil)

	got, err := service.ensureProcessNodesForDefinition(context.Background(), definition, user)
	if err != nil || len(got) != 1 || len(persistence.inserts) != 1 {
		t.Fatalf("[]byte metadata repair = (%#v, %v, inserts=%d), want one repaired node", got, err, len(persistence.inserts))
	}
}

type processNodeRepairPersistence struct {
	t8PersistenceStub
	inserts []processNodeRepairInsert
}

type processNodeRepairInsert struct {
	tableName string
	record    models.SObject
}

func (p *processNodeRepairPersistence) Insert(_ context.Context, tableName string, record models.SObject, _ *models.UserSession) (models.SObject, error) {
	inserted := cloneAutomationSObject(record)
	inserted[constants.FieldID] = "pn-repaired"
	p.inserts = append(p.inserts, processNodeRepairInsert{tableName: tableName, record: inserted})
	return inserted, nil
}

func (p *processNodeRepairPersistence) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}
