package catalogsnapshot

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"
	"testing"

	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
)

// CompileInput accepts declared capability flags before policy narrowing. All
// published create requirements must still agree with the mutation projection.
func TestCreateInputRequirementsRespectWriteProtection(t *testing.T) {
	policies := []struct {
		name, value string
		canCreate   bool
	}{
		{"ordinary", "", true},
		{"immutable_after_create", constants.FieldWriteProtectionImmutableAfterCreate, true},
		{"trusted_only", constants.FieldWriteProtectionTrustedOnly, false},
		{"authority_only", constants.FieldWriteProtectionAuthorityOnly, false},
		{"unknown", "future_unknown_policy", false},
		{"padded_trusted_only", " trusted_only ", false},
		{"padded_immutable_after_create", " immutable_after_create ", true},
	}
	for _, policy := range policies {
		for _, declared := range []bool{true, false} {
			for _, defaultKind := range []string{"none", "literal", "autonumber"} {
				t.Run(fmt.Sprintf("%s/createable=%t/default=%s", policy.name, declared, defaultKind), func(t *testing.T) {
					input := layoutAuthoringCompileInput()
					for _, object := range input.Objects {
						if object.Metadata.APIName == "WorkItem" {
							field := models.FindField(object.Metadata, "Subject")
							field.Required = true
							field.Createable, field.Updateable = declared, declared
							field.WriteProtectionPolicy = policy.value
							if defaultKind == "literal" {
								value := "server-value"
								field.DefaultValue = &value
							}
						}
					}
					if defaultKind == "autonumber" {
						input.MutationDefinitions = []MutationDefinitionInput{{CatalogRevision: testRevision, AutoNumberRule: &AutoNumberRule{
							FieldID: "field-subject", ObjectAPIName: "WorkItem", FieldAPIName: "Subject",
							Format: "WI-{0000}", Start: 1, Scope: "global", DateSource: "assigned_at", OnChange: "keep_existing",
						}}}
					}
					input.Projections = append(input.Projections, RuntimeProjectionInput{
						CatalogRevision: testRevision, AppInstallationID: testInstallA,
						CapabilityID: testMutationCapability, Profile: ProjectionRecordMutationV1,
						ObjectAPINames: []string{"WorkItem", "Workspace", "Unrelated"},
					})
					sourceBefore, err := json.Marshal(input.Objects)
					if err != nil {
						t.Fatal(err)
					}
					want := []string{}
					if declared && policy.canCreate && defaultKind == "none" {
						want = []string{"Subject"}
					}
					effective, err := BuildEffectiveMetadata(input)
					if err != nil {
						t.Fatal(err)
					}
					before := effective.Bytes()
					required, err := effective.CreateRequiredInputFields()
					if err != nil {
						t.Fatal(err)
					}
					if !reflect.DeepEqual(required["WorkItem"], want) {
						t.Errorf("component requirements=%#v, want=%#v", required["WorkItem"], want)
					}
					artifact, err := CompileEffectiveMetadata(effective)
					if err != nil {
						t.Fatal(err)
					}
					snapshot := mustOpenSnapshot(t, artifact)
					layout := mustOpenView(t, mustProject(t, snapshot, testInstallA, layoutauthoringv1.CapabilityName))
					stored, ok := layout.LayoutAuthoringStoredLayout("layout-work-item")
					if !ok || stored.CreateContext == nil || !reflect.DeepEqual(stored.CreateContext.RequiredFields, want) {
						t.Errorf("stored requirements=%#v, want=%#v", stored.CreateContext, want)
					}
					mutation := mustOpenView(t, mustProject(t, snapshot, testInstallA, testMutationCapability))
					object, _ := mutation.Object("WorkItem")
					if got := schemautil.RequiredCreateInputFields(object); !reflect.DeepEqual(got, want) {
						t.Errorf("mutation requirements=%#v, want=%#v", got, want)
					}
					sourceAfter, err := json.Marshal(input.Objects)
					if err != nil {
						t.Fatal(err)
					}
					if !bytes.Equal(sourceBefore, sourceAfter) || !bytes.Equal(before, effective.Bytes()) {
						t.Fatal("derivation modified immutable input")
					}
					required["WorkItem"] = []string{"ModifiedByCaller"}
					again, err := effective.CreateRequiredInputFields()
					if err != nil || !reflect.DeepEqual(again["WorkItem"], want) {
						t.Errorf("returned list changed source: %#v %v", again, err)
					}
				})
			}
		}
	}
}

func TestCreateInputWritePolicyChangesPreserveLayoutVersions(t *testing.T) {
	var previous *layoutauthoringv1.StoredLayout
	for step, policy := range []string{
		"", constants.FieldWriteProtectionTrustedOnly, constants.FieldWriteProtectionAuthorityOnly,
		constants.FieldWriteProtectionImmutableAfterCreate, "future_unknown_policy", "",
	} {
		input := layoutAuthoringCompileInput()
		for _, object := range input.Objects {
			if object.Metadata.APIName == "WorkItem" {
				field := models.FindField(object.Metadata, "Subject")
				field.Required, field.Createable = true, true
				field.WriteProtectionPolicy = policy
			}
		}
		revision := testRevision + CatalogRevision(step)
		input = withRevision(input, revision)
		for i := range input.LayoutAuthoringAggregates {
			input.LayoutAuthoringAggregates[i].CatalogRevision = revision
		}
		snapshot := mustOpenSnapshot(t, mustCompile(t, input))
		view := mustOpenView(t, mustProject(t, snapshot, testInstallA, layoutauthoringv1.CapabilityName))
		stored, ok := view.LayoutAuthoringStoredLayout("layout-work-item")
		if !ok || stored.CreateContext == nil {
			t.Fatal("missing stored Layout create context")
		}
		want := []string{}
		if policy == "" || policy == constants.FieldWriteProtectionImmutableAfterCreate {
			want = []string{"Subject"}
		}
		if !reflect.DeepEqual(stored.CreateContext.RequiredFields, want) {
			t.Errorf("policy=%q required=%v, want=%v", policy, stored.CreateContext.RequiredFields, want)
		}
		if previous != nil && (stored.ResourceVersion != previous.ResourceVersion ||
			stored.AggregateVersion != previous.AggregateVersion || stored.WorkspaceVersion != previous.WorkspaceVersion ||
			!reflect.DeepEqual(stored.Document, previous.Document)) {
			t.Fatal("write policy change modified Layout document or write versions")
		}
		previous = &stored
	}
}
