package records

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

	eventFeature "github.com/nexuscrm/events"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/validation"
	metadataFeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

// treeInsertRecord captures one repo-level insert in the order it was written,
// with the record image (including the id the engine assigned before repo.Insert)
// so tree tests can assert parent-before-child ordering and FK stamping.
type treeInsertRecord struct {
	table  string
	record models.SObject
}

// treeRecordingRepo is a stubUpsertRecordRepo whose Insert records every write in
// order. Insert errors can be injected per object type to exercise the atomic
// tree-abort path. FindOne/GetLock return the recorded insert image so any
// post-insert re-read inside the engine still resolves the just-created row.
type treeRecordingRepo struct {
	stubUpsertRecordRepo
	inserts    []treeInsertRecord
	failObject string
}

func (r *treeRecordingRepo) Insert(ctx context.Context, table string, rec models.SObject) error {
	if r.failObject != "" && strings.EqualFold(table, r.failObject) {
		return fmt.Errorf("injected insert failure for %s", table)
	}
	r.inserts = append(r.inserts, treeInsertRecord{table: table, record: cloneSObject(rec)})
	return nil
}

func (r *treeRecordingRepo) FindOne(ctx context.Context, table string, id string) (models.SObject, error) {
	for _, ins := range r.inserts {
		if ins.record[constants.FieldID] == id {
			return cloneSObject(ins.record), nil
		}
	}
	return nil, nil
}

func (r *treeRecordingRepo) GetLock(ctx context.Context, table string, id string) (models.SObject, error) {
	return r.FindOne(ctx, table, id)
}

// treeParentSchema / treeChildSchema define a minimal parent object and a child
// object whose "ParentRef" lookup points back at the parent. The child field's
// relationship name is "Children", so a tree keyed by "Children" resolves its FK
// to ParentRef via the same metadata-driven resolveFK the SF path uses.
func treeParentSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		ID:           "obj_tree_parent",
		APIName:      "TreeParent",
		Label:        "Tree Parent",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
		},
	}
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

func treeChildSchema() *models.ObjectMetadata {
	relName := "Children"
	schema := &models.ObjectMetadata{
		ID:           "obj_tree_child",
		APIName:      "TreeChild",
		Label:        "Tree Child",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{
				APIName:          "ParentRef",
				Type:             constants.FieldTypeReference,
				ReferenceTo:      []string{"TreeParent"},
				RelationshipName: &relName,
				Createable:       true,
				Updateable:       true,
			},
		},
	}
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

// newTreeTestService mirrors newRecordEntrypointTestService but seeds the metadata
// service with the full parent+child schema set so relationship/FK resolution works.
func newTreeTestService(repo *treeRecordingRepo) *PersistenceService {
	metaRepo := &stubUpsertMetaRepo{schemas: []*models.ObjectMetadata{treeParentSchema(), treeChildSchema()}}
	metaSvc := metadataFeature.NewMetadataService(metaRepo, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	ps := &PersistenceService{
		repo:                repo,
		rollup:              noopRollupProcessor{},
		metadata:            metaSvc,
		permissions:         permSvc,
		eventBus:            &eventFeature.NoOpEventBus{},
		validator:           metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator:     validation.NewSchemaValidator(),
		txManager:           inlineTxManager{},
		cacheInvalidations:  newRecordCacheInvalidations(),
		writeCanonicalizers: newRecordWriteCanonicalizers(),
	}
	ps.registerBuiltInSecurityCacheInvalidations()
	ps.registerBuiltInSecurityWriteCanonicalizers()
	ps.registerBuiltInSecurityWriteValidators()
	return ps
}

// TestCreateTreeNodesInsertsParentBeforeChildAndResolvesFK proves the neutral
// typed-IR path inserts the parent before the child, resolves the child's FK to
// the parent's freshly created id, and returns a TreeCreateResult mapping each
// ReferenceID to its new id.
func TestCreateTreeNodesInsertsParentBeforeChildAndResolvesFK(t *testing.T) {
	repo := &treeRecordingRepo{}
	ps := newTreeTestService(repo)

	nodes := []contract.TreeNode{{
		ObjectType:  "TreeParent",
		ReferenceID: "p1",
		Fields:      contract.Record{constants.FieldName: "root"},
		Children: map[string][]contract.TreeNode{
			"Children": {{
				ObjectType:  "TreeChild",
				ReferenceID: "c1",
				Fields:      contract.Record{constants.FieldName: "leaf"},
			}},
		},
	}}

	results, err := ps.CreateTreeNodes(context.Background(), nodes, recordEntrypointUser())
	if err != nil {
		t.Fatalf("CreateTreeNodes: %v", err)
	}

	// Two inserts, parent first.
	if len(repo.inserts) != 2 {
		t.Fatalf("inserts = %d, want 2: %#v", len(repo.inserts), repo.inserts)
	}
	if repo.inserts[0].table != "TreeParent" {
		t.Fatalf("first insert table = %q, want TreeParent", repo.inserts[0].table)
	}
	if repo.inserts[1].table != "TreeChild" {
		t.Fatalf("second insert table = %q, want TreeChild", repo.inserts[1].table)
	}

	// The child's ParentRef FK is the parent's freshly created id.
	parentID, _ := repo.inserts[0].record[constants.FieldID].(string)
	if parentID == "" {
		t.Fatalf("parent insert has no id: %#v", repo.inserts[0].record)
	}
	childFK, _ := repo.inserts[1].record["ParentRef"].(string)
	if childFK != parentID {
		t.Fatalf("child ParentRef = %q, want parent id %q; child=%#v", childFK, parentID, repo.inserts[1].record)
	}

	// Result maps ReferenceID -> new id; root ReferenceID echoes "p1" with the
	// parent's id and no errors.
	if len(results) != 1 {
		t.Fatalf("results = %d, want 1 (one root node): %#v", len(results), results)
	}
	root := results[0]
	if root.ReferenceID != "p1" {
		t.Fatalf("root ReferenceID = %q, want p1", root.ReferenceID)
	}
	if root.ID != parentID {
		t.Fatalf("root ID = %q, want parent id %q", root.ID, parentID)
	}
	if len(root.Errors) != 0 {
		t.Fatalf("root Errors = %#v, want empty", root.Errors)
	}
}

// TestCreateTreeNodesAtomicOnChildFailure proves the neutral path is atomic: a
// child insert failure aborts the whole tree with an error (Tree API is atomic).
func TestCreateTreeNodesAtomicOnChildFailure(t *testing.T) {
	repo := &treeRecordingRepo{failObject: "TreeChild"}
	ps := newTreeTestService(repo)

	nodes := []contract.TreeNode{{
		ObjectType:  "TreeParent",
		ReferenceID: "p1",
		Fields:      contract.Record{constants.FieldName: "root"},
		Children: map[string][]contract.TreeNode{
			"Children": {{
				ObjectType: "TreeChild",
				Fields:     contract.Record{constants.FieldName: "leaf"},
			}},
		},
	}}

	results, err := ps.CreateTreeNodes(context.Background(), nodes, recordEntrypointUser())
	if err == nil {
		t.Fatalf("CreateTreeNodes returned nil error on child insert failure; results=%#v", results)
	}
	if !strings.Contains(err.Error(), "injected insert failure for TreeChild") {
		t.Fatalf("error = %v, want injected child insert failure", err)
	}
	if results != nil {
		t.Fatalf("results = %#v, want nil on error", results)
	}
	// The child insert failed before it was recorded; the parent insert did run.
	// The transaction body returns the error so the outer boundary rolls back.
	for _, ins := range repo.inserts {
		if ins.table == "TreeChild" {
			t.Fatalf("child insert was recorded despite injected failure: %#v", ins)
		}
	}
}

// TestCreateTreeNodesRejectsGenericListView proves the neutral typed-IR path
// applies the generic-list-view mutation guard: a tree whose root node is the
// _System_ListView system object is rejected with an invalid-operation error and
// nothing is inserted. The root object type is the root node's self-described
// ObjectType, which the object-name closure feeds into rejectGenericListViewMutation
// under the !BypassesCRUDandFLS() condition. This pins that the guard covers the
// root node's own object type (not only nested children).
func TestCreateTreeNodesRejectsGenericListView(t *testing.T) {
	ctx := context.Background()
	user := recordEntrypointUser()

	neutralRepo := &treeRecordingRepo{}
	neutralPS := newTreeTestService(neutralRepo)
	neutralNodes := []contract.TreeNode{{
		ObjectType: constants.TableListView,
		Fields:     contract.Record{constants.FieldName: "some list view"},
	}}
	_, neutralErr := neutralPS.CreateTreeNodes(ctx, neutralNodes, user)
	if !isListViewInvalidOperation(neutralErr) {
		t.Fatalf("CreateTreeNodes(_System_ListView) error = %T %v, want list-view invalid operation", neutralErr, neutralErr)
	}
	for _, ins := range neutralRepo.inserts {
		t.Fatalf("CreateTreeNodes(_System_ListView) inserted a row despite guard: %#v", ins)
	}
}

// TestCreateTreeNodesInsertTreeNodeSemantics pins the neutral typed-IR path's
// insert semantics: for a parent node with one child relationship, the engine
// inserts parent-then-child, resolves the child's FK to the parent's freshly
// created id, and returns one root result whose ReferenceID echoes the root
// node's alias and whose ID equals the created parent id. Record ids are
// engine-generated, so the assertions are on structure (ordering + FK-linkage +
// ReferenceID echo + id mapping), not on literal id values.
func TestCreateTreeNodesInsertTreeNodeSemantics(t *testing.T) {
	neutralRepo := &treeRecordingRepo{}
	neutralPS := newTreeTestService(neutralRepo)
	neutralNodes := []contract.TreeNode{{
		ObjectType:  "TreeParent",
		ReferenceID: "p1",
		Fields:      contract.Record{constants.FieldName: "root"},
		Children: map[string][]contract.TreeNode{
			"Children": {{
				ObjectType:  "TreeChild",
				ReferenceID: "c1",
				Fields:      contract.Record{constants.FieldName: "leaf"},
			}},
		},
	}}
	neutralResults, err := neutralPS.CreateTreeNodes(context.Background(), neutralNodes, recordEntrypointUser())
	if err != nil {
		t.Fatalf("neutral CreateTreeNodes: %v", err)
	}

	// Insert ordering (parent then child) with the child's FK linked to the
	// parent's freshly created id.
	inserts := neutralRepo.inserts
	if len(inserts) != 2 {
		t.Fatalf("inserts = %d, want 2: %#v", len(inserts), inserts)
	}
	if inserts[0].table != "TreeParent" || inserts[1].table != "TreeChild" {
		t.Fatalf("insert order = [%s, %s], want [TreeParent, TreeChild]", inserts[0].table, inserts[1].table)
	}
	parentID, _ := inserts[0].record[constants.FieldID].(string)
	childFK, _ := inserts[1].record["ParentRef"].(string)
	if parentID == "" {
		t.Fatalf("parent insert has no id: %#v", inserts[0].record)
	}
	if childFK != parentID {
		t.Fatalf("child ParentRef = %q, want parent id %q", childFK, parentID)
	}

	// One root result: ReferenceID echoes the root alias, ID maps to the created
	// parent id.
	if len(neutralResults) != 1 {
		t.Fatalf("result length = %d, want 1", len(neutralResults))
	}
	if neutralResults[0].ReferenceID != "p1" {
		t.Fatalf("referenceId echo = %q, want %q", neutralResults[0].ReferenceID, "p1")
	}
	if neutralResults[0].ID != parentID {
		t.Fatalf("neutral result id = %q, want parent id %q", neutralResults[0].ID, parentID)
	}
}
