package catalogreconcile

import (
	"context"
	"encoding/json"
	"errors"
	"reflect"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	consumerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

func TestCreateInputPublicationUsesMutationDefaultsForEveryConsumer(t *testing.T) {
	for _, mode := range []string{"layout only", "app metadata only", "combined"} {
		t.Run(mode, func(t *testing.T) {
			object := validObject("WorkItem")
			object.Fields = append(object.Fields, models.FieldMetadata{ID: "field-title", APIName: "Title", Label: "Title", Type: constants.FieldTypeString, PhysicalName: "title", Required: true, Createable: true})
			base := &markedReader{t: t, objects: []*models.ObjectMetadata{object}, layouts: map[string][]*models.PageLayout{"WorkItem": {{ID: "stored-layout", ObjectAPIName: "WorkItem", LayoutName: "Stored", Type: "Detail"}}}, autoNumberRules: []catalogsnapshot.AutoNumberRule{{FieldID: "field-title", ObjectAPIName: "WorkItem", FieldAPIName: "Title", Format: "WI-{0000}", Start: 1, Scope: "global", DateSource: "assigned_at", OnChange: "keep_existing"}}}
			reader := &completeV3FlowReader{markedReader: base}
			capabilities := []string{layoutauthoringv1.CapabilityName}
			appCapability := ""
			if mode == "app metadata only" {
				capabilities = []string{queryv1.CapabilityName}
				appCapability = consumerv1.AppMetadataV3CapabilityName
			}
			if mode == "combined" {
				capabilities = append(capabilities, recordmutationv1.CapabilityName)
				appCapability = consumerv1.AppMetadataV3CapabilityName
			}
			release := verifiedPlatformRelease(t, nil)
			selection := MetadataSelection{OrgID: "org-test", AppInstallationID: "installation-test", RuntimeCapabilityIDs: capabilities, CatalogRevision: 7, SchemaGeneration: 8, EngineGeneration: 9}
			for _, hasRule := range []bool{true, false} {
				if !hasRule {
					base.autoNumberRules = nil
					selection.CatalogRevision++
				}
				base.autoNumberReads = 0
				result, err := readEffectiveMetadata(t.Context(), func(ctx context.Context, fn func(context.Context) error) error {
					return fn(context.WithValue(ctx, transactionMarker{}, "one-snapshot"))
				}, reader, selection, &release, appCapability)
				if err != nil {
					t.Fatal(err)
				}
				if base.autoNumberReads != 1 {
					t.Fatalf("autonumber reads=%d want one", base.autoNumberReads)
				}
				if mode == "layout only" && (base.flowReadCalls != 0 || base.fieldMappingCalls != 0) {
					t.Fatal("read-only requirements loaded unrelated mutation metadata")
				}
				want := []string{}
				if !hasRule {
					want = []string{"Title"}
				}
				required, err := result.metadata.CreateRequiredInputFields()
				if err != nil || !reflect.DeepEqual(required["WorkItem"], want) {
					t.Fatalf("effective requirements=%#v err=%v", required, err)
				}
				artifact, err := catalogsnapshot.CompileEffectiveMetadata(result.metadata)
				if err != nil {
					t.Fatal(err)
				}
				snapshot, err := catalogsnapshot.OpenCatalogSnapshot(artifact)
				if err != nil {
					t.Fatal(err)
				}
				for _, capability := range capabilities {
					artifact, err := catalogsnapshot.ProjectRuntimeMetadataView(snapshot, catalogsnapshot.ProjectionKey{AppInstallationID: "installation-test", CapabilityID: capability})
					if err != nil {
						t.Fatal(err)
					}
					view, err := catalogsnapshot.OpenRuntimeMetadataView(artifact)
					if err != nil {
						t.Fatal(err)
					}
					switch capability {
					case layoutauthoringv1.CapabilityName:
						stored, ok := view.LayoutAuthoringStoredLayout("stored-layout")
						if !ok || stored.CreateContext == nil || !reflect.DeepEqual(stored.CreateContext.RequiredFields, want) {
							t.Fatalf("stored create context=%#v", stored.CreateContext)
						}
					case recordmutationv1.CapabilityName:
						effectiveObject, _ := view.Object("WorkItem")
						if !reflect.DeepEqual(schemautil.RequiredCreateInputFields(effectiveObject), want) {
							t.Fatal("producer differs from mutation schema")
						}
					}
				}
				if appCapability != "" {
					found := false
					for _, component := range result.components {
						if component.ComponentKind != consumerv1.EffectiveObjectSearchComponentKind {
							continue
						}
						var document effectiveObjectSearchDocument
						if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
							t.Fatal(err)
						}
						if len(document.Objects) != 1 || document.Objects[0].CreateRequiredFields == nil || !reflect.DeepEqual(*document.Objects[0].CreateRequiredFields, want) {
							t.Fatalf("search create context=%s", component.CanonicalBehavior)
						}
						found = true
					}
					if !found {
						t.Fatal("object-search component missing")
					}
				}
				if models.FindField(object, "Title").DefaultedOnCreate {
					t.Fatal("derived fact changed raw source")
				}
			}
		})
	}
}

func TestObjectSearchCreateRequirementsRejectUnavailableContext(t *testing.T) {
	object := validObject("WorkItem")
	for _, missing := range []map[string][]string{nil, {}, {"WorkItem": nil}} {
		if _, err := buildEffectiveObjectSearchComponent([]*models.ObjectMetadata{object}, nil, consumerv1.AppMetadataV3CapabilityName, missing); err == nil {
			t.Fatal("unavailable requirements became empty")
		}
	}
	if _, err := buildEffectiveObjectSearchComponent([]*models.ObjectMetadata{object}, nil, consumerv1.AppMetadataV3CapabilityName, map[string][]string{"WorkItem": {}}); err != nil {
		t.Fatal(err)
	}
}

func TestCreateInputPublicationRejectsUnavailableDefaultAuthority(t *testing.T) {
	base := &markedReader{t: t, objects: []*models.ObjectMetadata{validObject("WorkItem")}}
	unavailable := errors.New("autonumber source unavailable")
	for _, test := range []struct {
		name   string
		reader MetadataReader
		cause  error
	}{
		{"missing reader", struct {
			MetadataReader
			LayoutAuthoringMetadataReader
		}{base, base}, nil},
		{"failed read", createInputFailingReader{base, unavailable}, unavailable},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, err := ReadEffectiveMetadata(t.Context(), func(ctx context.Context, read func(context.Context) error) error {
				return read(context.WithValue(ctx, transactionMarker{}, "one-snapshot"))
			}, test.reader, MetadataSelection{OrgID: "org-test", AppInstallationID: "installation-test", RuntimeCapabilityID: layoutauthoringv1.CapabilityName, CatalogRevision: 7, SchemaGeneration: 8, EngineGeneration: 9}, nil)
			if err == nil || !strings.Contains(err.Error(), "autonumber") {
				t.Fatalf("missing source became known empty: %v", err)
			}
			if test.cause != nil && !errors.Is(err, test.cause) {
				t.Fatalf("lost source failure: %v", err)
			}
		})
	}
}

type createInputFailingReader struct {
	*markedReader
	failure error
}

func (r createInputFailingReader) GetAutoNumberRules(ctx context.Context) ([]catalogsnapshot.AutoNumberRule, error) {
	r.requireTransaction(ctx)
	return nil, r.failure
}
